opencode-telegram-bot 1.0.10 → 1.1.1
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/README.md +30 -1
- package/dist/app.d.ts +7 -1
- package/dist/index.js +307 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ These are handled directly by the Telegram bot:
|
|
|
112
112
|
| `/model` | Show current model and usage hints |
|
|
113
113
|
| `/model <keyword>` | Search models by keyword |
|
|
114
114
|
| `/model default` | Reset to the default model |
|
|
115
|
+
| `/agent` | Show current agent and switch (plan, build, ...) |
|
|
116
|
+
| `/agent <name>` | Switch directly to a named agent |
|
|
115
117
|
| `/usage` | Show token and cost usage for this session |
|
|
116
118
|
| `/help` | Show available commands |
|
|
117
119
|
|
|
@@ -177,6 +179,33 @@ Other commands:
|
|
|
177
179
|
- `/model` shows the current model and usage hints
|
|
178
180
|
- `/model default` resets to the server default
|
|
179
181
|
|
|
182
|
+
### Agent Switching
|
|
183
|
+
|
|
184
|
+
OpenCode supports multiple agents: **build** (default, full edit access), **plan** (read-only analysis), and others. Use `/agent` to see and switch between them:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
You: /agent
|
|
188
|
+
Bot: Current agent: build
|
|
189
|
+
|
|
190
|
+
Available agents:
|
|
191
|
+
- build (active) -- The default agent
|
|
192
|
+
- plan -- Plan mode. Disallows all edit tools.
|
|
193
|
+
- general -- General-purpose agent
|
|
194
|
+
- explore -- Codebase explorer
|
|
195
|
+
|
|
196
|
+
[build] [plan] [general] [explore]
|
|
197
|
+
|
|
198
|
+
You: [tap "plan"]
|
|
199
|
+
(button message is deleted)
|
|
200
|
+
Bot: Agent: plan | Model: claude-opus-4 | Verbose: off [pinned]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pinned Status Message
|
|
204
|
+
|
|
205
|
+
A pinned message at the top of the chat shows the current agent, model, and verbose mode. It updates automatically when any of these change — via `/agent`, `/model`, or `/verbose`. The pinned message is the only confirmation; no separate reply is sent.
|
|
206
|
+
|
|
207
|
+
Agent, model, and verbose selections are per-chat and persist across bot restarts.
|
|
208
|
+
|
|
180
209
|
## Usage
|
|
181
210
|
|
|
182
211
|
Use `/usage` to see the current session's token counts and estimated cost:
|
|
@@ -251,7 +280,7 @@ You: [tap "Auth refactoring"]
|
|
|
251
280
|
Bot: Switched to session: Auth refactoring
|
|
252
281
|
```
|
|
253
282
|
|
|
254
|
-
The `/sessions` list
|
|
283
|
+
The `/sessions` list shows sessions created by the Telegram bot. At the bottom of the list, a **"Show all sessions"** button lets you discover sessions created by other OpenCode clients (like the TUI via `opencode attach`). Tapping an external session adopts it and switches to it. You cannot delete the currently active session -- use `/new` first.
|
|
255
284
|
|
|
256
285
|
## Session Persistence
|
|
257
286
|
|
package/dist/app.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ interface OpencodeClientLike {
|
|
|
26
26
|
reply: (params: any, options?: any) => Promise<any>;
|
|
27
27
|
reject: (params: any, options?: any) => Promise<any>;
|
|
28
28
|
};
|
|
29
|
+
app: {
|
|
30
|
+
agents: (params?: any, options?: any) => Promise<any>;
|
|
31
|
+
};
|
|
29
32
|
}
|
|
30
33
|
interface StartOptions {
|
|
31
34
|
url: string;
|
|
@@ -48,7 +51,7 @@ interface TelegramBot {
|
|
|
48
51
|
}) => Promise<void>;
|
|
49
52
|
stop: (reason?: string) => void;
|
|
50
53
|
telegram: {
|
|
51
|
-
sendMessage: (chatId: number, text: string, options?: Record<string, unknown>) => Promise<
|
|
54
|
+
sendMessage: (chatId: number, text: string, options?: Record<string, unknown>) => Promise<any>;
|
|
52
55
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
53
56
|
sendDocument: (chatId: number, file: {
|
|
54
57
|
source: Buffer;
|
|
@@ -61,6 +64,9 @@ interface TelegramBot {
|
|
|
61
64
|
command: string;
|
|
62
65
|
description: string;
|
|
63
66
|
}>) => Promise<unknown>;
|
|
67
|
+
pinChatMessage: (chatId: number, messageId: number, extra?: Record<string, unknown>) => Promise<void>;
|
|
68
|
+
unpinChatMessage: (chatId: number, messageId?: number) => Promise<void>;
|
|
69
|
+
editMessageText: (chatId: number | undefined, messageId: number | undefined, inlineMessageId: string | undefined, text: string, extra?: Record<string, unknown>) => Promise<void>;
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
export declare function startTelegram(options: StartOptions): Promise<TelegramBot>;
|
package/dist/index.js
CHANGED
|
@@ -19967,6 +19967,8 @@ async function startTelegram(options) {
|
|
|
19967
19967
|
const sessionsFile = options.sessionsFilePath || SESSIONS_FILE;
|
|
19968
19968
|
// Initialize OpenCode client
|
|
19969
19969
|
const client = options.client || client_createOpencodeClient({ baseUrl: url });
|
|
19970
|
+
// Available agents fetched from OpenCode on startup
|
|
19971
|
+
let availableAgents = [];
|
|
19970
19972
|
// Verify connection to the OpenCode server and fetch available commands
|
|
19971
19973
|
const opencodeCommands = new Set();
|
|
19972
19974
|
const opencodeCommandMenu = [];
|
|
@@ -20004,13 +20006,25 @@ async function startTelegram(options) {
|
|
|
20004
20006
|
}
|
|
20005
20007
|
console.log(`[Telegram] Available OpenCode commands: ${[...opencodeCommands].join(", ")}`);
|
|
20006
20008
|
}
|
|
20009
|
+
// Fetch available agents
|
|
20010
|
+
const agentsResult = await client.app.agents({});
|
|
20011
|
+
if (agentsResult.data) {
|
|
20012
|
+
availableAgents = agentsResult.data
|
|
20013
|
+
.filter((a) => !a.hidden)
|
|
20014
|
+
.map((a) => ({
|
|
20015
|
+
name: a.name,
|
|
20016
|
+
description: a.description || "",
|
|
20017
|
+
mode: a.mode || "primary",
|
|
20018
|
+
}));
|
|
20019
|
+
console.log(`[Telegram] Available agents: ${availableAgents.map((a) => `${a.name} (${a.mode})`).join(", ")}`);
|
|
20020
|
+
}
|
|
20007
20021
|
}
|
|
20008
20022
|
catch (err) {
|
|
20009
20023
|
throw new Error(`[Telegram] Failed to connect to OpenCode server at ${url}. Make sure it's running (npm run serve). Error: ${err}`);
|
|
20010
20024
|
}
|
|
20011
20025
|
// Telegram-only commands that should not be forwarded to OpenCode
|
|
20012
20026
|
const telegramCommands = new Set([
|
|
20013
|
-
"start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose", "model", "usage",
|
|
20027
|
+
"start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose", "model", "usage", "agent",
|
|
20014
20028
|
]);
|
|
20015
20029
|
const telegramCommandMenu = [
|
|
20016
20030
|
{ command: "new", description: "Start a new conversation" },
|
|
@@ -20019,6 +20033,7 @@ async function startTelegram(options) {
|
|
|
20019
20033
|
{ command: "export", description: "Export session (/export full for details)" },
|
|
20020
20034
|
{ command: "verbose", description: "Toggle verbose mode" },
|
|
20021
20035
|
{ command: "model", description: "Search models (/model <keyword>)" },
|
|
20036
|
+
{ command: "agent", description: "Switch agent (plan, build, ...)" },
|
|
20022
20037
|
{ command: "usage", description: "Show token and cost usage" },
|
|
20023
20038
|
{ command: "help", description: "Show available commands" },
|
|
20024
20039
|
];
|
|
@@ -20031,6 +20046,10 @@ async function startTelegram(options) {
|
|
|
20031
20046
|
const chatVerboseMode = new Set();
|
|
20032
20047
|
// Map of chatId -> model override (provider/model)
|
|
20033
20048
|
const chatModelOverride = new Map();
|
|
20049
|
+
// Map of chatId -> agent override (e.g. "plan", "build")
|
|
20050
|
+
const chatAgentOverride = new Map();
|
|
20051
|
+
// Map of chatId -> pinned status message ID
|
|
20052
|
+
const chatPinnedStatusMsg = new Map();
|
|
20034
20053
|
// Map of chatId -> last search results (in-memory only)
|
|
20035
20054
|
const chatModelSearchResults = new Map();
|
|
20036
20055
|
// Map of questionId -> pending question context (for forwarding OpenCode questions to Telegram)
|
|
@@ -20045,6 +20064,8 @@ async function startTelegram(options) {
|
|
|
20045
20064
|
known: [...knownSessionIds],
|
|
20046
20065
|
verbose: [...chatVerboseMode],
|
|
20047
20066
|
models: Object.fromEntries(chatModelOverride),
|
|
20067
|
+
agents: Object.fromEntries(chatAgentOverride),
|
|
20068
|
+
pinnedStatus: Object.fromEntries(chatPinnedStatusMsg),
|
|
20048
20069
|
};
|
|
20049
20070
|
(0,external_node_fs_.writeFileSync)(sessionsFile, JSON.stringify(data, null, 2));
|
|
20050
20071
|
}
|
|
@@ -20062,6 +20083,8 @@ async function startTelegram(options) {
|
|
|
20062
20083
|
let storedKnown = [];
|
|
20063
20084
|
let storedVerbose = [];
|
|
20064
20085
|
let storedModels = {};
|
|
20086
|
+
let storedAgents = {};
|
|
20087
|
+
let storedPinnedStatus = {};
|
|
20065
20088
|
if ((0,external_node_fs_.existsSync)(sessionsFile)) {
|
|
20066
20089
|
try {
|
|
20067
20090
|
const raw = (0,external_node_fs_.readFileSync)(sessionsFile, "utf-8");
|
|
@@ -20072,6 +20095,8 @@ async function startTelegram(options) {
|
|
|
20072
20095
|
storedKnown = parsed.known || [];
|
|
20073
20096
|
storedVerbose = parsed.verbose || [];
|
|
20074
20097
|
storedModels = parsed.models || {};
|
|
20098
|
+
storedAgents = parsed.agents || {};
|
|
20099
|
+
storedPinnedStatus = parsed.pinnedStatus || {};
|
|
20075
20100
|
}
|
|
20076
20101
|
else {
|
|
20077
20102
|
// Old format: flat { chatId: sessionId }
|
|
@@ -20096,6 +20121,18 @@ async function startTelegram(options) {
|
|
|
20096
20121
|
chatModelOverride.set(chatId, modelId);
|
|
20097
20122
|
}
|
|
20098
20123
|
}
|
|
20124
|
+
// Restore agent overrides
|
|
20125
|
+
for (const [chatId, agentName] of Object.entries(storedAgents)) {
|
|
20126
|
+
if (agentName) {
|
|
20127
|
+
chatAgentOverride.set(chatId, agentName);
|
|
20128
|
+
}
|
|
20129
|
+
}
|
|
20130
|
+
// Restore pinned status message IDs
|
|
20131
|
+
for (const [chatId, msgId] of Object.entries(storedPinnedStatus)) {
|
|
20132
|
+
if (msgId) {
|
|
20133
|
+
chatPinnedStatusMsg.set(chatId, msgId);
|
|
20134
|
+
}
|
|
20135
|
+
}
|
|
20099
20136
|
// Fetch all server sessions once for validation and fallback matching
|
|
20100
20137
|
let serverSessions = [];
|
|
20101
20138
|
try {
|
|
@@ -20214,6 +20251,10 @@ async function startTelegram(options) {
|
|
|
20214
20251
|
const modelID = modelParts.join("/");
|
|
20215
20252
|
promptBody.model = { providerID, modelID };
|
|
20216
20253
|
}
|
|
20254
|
+
const agentOverride = chatAgentOverride.get(chatId);
|
|
20255
|
+
if (agentOverride) {
|
|
20256
|
+
promptBody.agent = agentOverride;
|
|
20257
|
+
}
|
|
20217
20258
|
return promptBody;
|
|
20218
20259
|
}
|
|
20219
20260
|
function getChatIdFromContext(ctx) {
|
|
@@ -20787,7 +20828,12 @@ async function startTelegram(options) {
|
|
|
20787
20828
|
if (!authorizedUserId) {
|
|
20788
20829
|
return next();
|
|
20789
20830
|
}
|
|
20790
|
-
|
|
20831
|
+
// Skip auth for service messages without a sender or from the bot itself
|
|
20832
|
+
// (e.g. pin notifications where ctx.from is the bot)
|
|
20833
|
+
if (!ctx.from || ctx.from.is_bot) {
|
|
20834
|
+
return next();
|
|
20835
|
+
}
|
|
20836
|
+
const userId = ctx.from.id.toString();
|
|
20791
20837
|
if (userId === authorizedUserId) {
|
|
20792
20838
|
return next();
|
|
20793
20839
|
}
|
|
@@ -20810,6 +20856,7 @@ async function startTelegram(options) {
|
|
|
20810
20856
|
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
20811
20857
|
"/verbose on|off - Set verbose mode explicitly\n" +
|
|
20812
20858
|
"/model <keyword> - Search available models\n" +
|
|
20859
|
+
"/agent - Switch agent (plan, build, ...)\n" +
|
|
20813
20860
|
"/usage - Show token and cost usage for this session\n" +
|
|
20814
20861
|
"/help - Show this help message\n";
|
|
20815
20862
|
const visibleOpenCodeCommands = getVisibleOpenCodeCommands();
|
|
@@ -20832,6 +20879,7 @@ async function startTelegram(options) {
|
|
|
20832
20879
|
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
20833
20880
|
"/verbose on|off - Set verbose mode explicitly\n" +
|
|
20834
20881
|
"/model <keyword> - Search available models\n" +
|
|
20882
|
+
"/agent - Switch agent (plan, build, ...)\n" +
|
|
20835
20883
|
"/usage - Show token and cost usage for this session\n" +
|
|
20836
20884
|
"/help - Show this help message\n";
|
|
20837
20885
|
const visibleOpenCodeCommands = getVisibleOpenCodeCommands();
|
|
@@ -20875,7 +20923,13 @@ async function startTelegram(options) {
|
|
|
20875
20923
|
try {
|
|
20876
20924
|
const sessions = await getKnownSessions();
|
|
20877
20925
|
if (sessions.length === 0) {
|
|
20878
|
-
await ctx.reply("No sessions found
|
|
20926
|
+
await ctx.reply("No sessions found.\n\nYou can discover sessions created outside this bot:", {
|
|
20927
|
+
reply_markup: {
|
|
20928
|
+
inline_keyboard: [
|
|
20929
|
+
[{ text: "Show all sessions", callback_data: "sessions:all" }],
|
|
20930
|
+
],
|
|
20931
|
+
},
|
|
20932
|
+
});
|
|
20879
20933
|
return;
|
|
20880
20934
|
}
|
|
20881
20935
|
const activeSession = sessions.find((session) => session.id === activeSessionId);
|
|
@@ -20884,25 +20938,33 @@ async function startTelegram(options) {
|
|
|
20884
20938
|
msgLines.push(`Current session: ${activeSession.title}`);
|
|
20885
20939
|
}
|
|
20886
20940
|
const otherSessions = sessions.filter((session) => session.id !== activeSessionId);
|
|
20887
|
-
|
|
20941
|
+
const keyboard = [];
|
|
20942
|
+
if (otherSessions.length > 0) {
|
|
20943
|
+
msgLines.push("Tap a session to switch or delete.");
|
|
20944
|
+
otherSessions.forEach((session) => {
|
|
20945
|
+
keyboard.push([
|
|
20946
|
+
{
|
|
20947
|
+
text: session.title,
|
|
20948
|
+
callback_data: `switch:${session.id}`,
|
|
20949
|
+
},
|
|
20950
|
+
{
|
|
20951
|
+
text: "Delete",
|
|
20952
|
+
callback_data: `delete:${session.id}`,
|
|
20953
|
+
},
|
|
20954
|
+
]);
|
|
20955
|
+
});
|
|
20956
|
+
}
|
|
20957
|
+
else {
|
|
20888
20958
|
msgLines.push("This is your only session.");
|
|
20889
|
-
await ctx.reply(msgLines.join("\n"));
|
|
20890
|
-
return;
|
|
20891
20959
|
}
|
|
20892
|
-
|
|
20893
|
-
|
|
20894
|
-
|
|
20895
|
-
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
{
|
|
20901
|
-
text: "Delete",
|
|
20902
|
-
callback_data: `delete:${session.id}`,
|
|
20903
|
-
},
|
|
20904
|
-
]);
|
|
20905
|
-
});
|
|
20960
|
+
// Always show "Show all sessions" button to discover sessions
|
|
20961
|
+
// created outside the Telegram bot (e.g. via opencode TUI)
|
|
20962
|
+
keyboard.push([
|
|
20963
|
+
{
|
|
20964
|
+
text: "Show all sessions",
|
|
20965
|
+
callback_data: "sessions:all",
|
|
20966
|
+
},
|
|
20967
|
+
]);
|
|
20906
20968
|
await ctx.reply(msgLines.join("\n"), {
|
|
20907
20969
|
reply_markup: {
|
|
20908
20970
|
inline_keyboard: keyboard,
|
|
@@ -21062,6 +21124,83 @@ async function startTelegram(options) {
|
|
|
21062
21124
|
await answerAndEdit(ctx, "Failed to delete session.");
|
|
21063
21125
|
}
|
|
21064
21126
|
});
|
|
21127
|
+
// Handle "Show all sessions" button - list sessions not tracked by the bot
|
|
21128
|
+
bot.action("sessions:all", async (ctx) => {
|
|
21129
|
+
const chatId = getChatIdFromContext(ctx);
|
|
21130
|
+
if (!chatId)
|
|
21131
|
+
return;
|
|
21132
|
+
try {
|
|
21133
|
+
// Delete the button message
|
|
21134
|
+
try {
|
|
21135
|
+
await ctx.deleteMessage();
|
|
21136
|
+
}
|
|
21137
|
+
catch {
|
|
21138
|
+
// Ignore if already deleted
|
|
21139
|
+
}
|
|
21140
|
+
await ctx.answerCbQuery();
|
|
21141
|
+
const list = await client.session.list({});
|
|
21142
|
+
const allSessions = (list.data || [])
|
|
21143
|
+
.filter((s) => !knownSessionIds.has(s.id) && !s.parentID)
|
|
21144
|
+
.sort((a, b) => b.time.updated - a.time.updated);
|
|
21145
|
+
if (allSessions.length === 0) {
|
|
21146
|
+
await ctx.reply("No other sessions found.");
|
|
21147
|
+
return;
|
|
21148
|
+
}
|
|
21149
|
+
const keyboard = [];
|
|
21150
|
+
allSessions.forEach((session) => {
|
|
21151
|
+
keyboard.push([
|
|
21152
|
+
{
|
|
21153
|
+
text: session.title,
|
|
21154
|
+
callback_data: `adopt:${session.id}`,
|
|
21155
|
+
},
|
|
21156
|
+
]);
|
|
21157
|
+
});
|
|
21158
|
+
await ctx.reply("Sessions created outside this bot.\nTap to adopt and switch:", {
|
|
21159
|
+
reply_markup: {
|
|
21160
|
+
inline_keyboard: keyboard,
|
|
21161
|
+
},
|
|
21162
|
+
});
|
|
21163
|
+
}
|
|
21164
|
+
catch (err) {
|
|
21165
|
+
console.error("[Telegram] Error listing all sessions:", err);
|
|
21166
|
+
await ctx.reply("Failed to list sessions.");
|
|
21167
|
+
}
|
|
21168
|
+
});
|
|
21169
|
+
// Handle adopt button - adopt an external session and switch to it
|
|
21170
|
+
bot.action(/^adopt:(.+)$/, async (ctx) => {
|
|
21171
|
+
const chatId = getChatIdFromContext(ctx);
|
|
21172
|
+
const sessionId = ctx.match?.[1];
|
|
21173
|
+
if (!chatId || !sessionId)
|
|
21174
|
+
return;
|
|
21175
|
+
try {
|
|
21176
|
+
// Verify the session still exists on the server
|
|
21177
|
+
const list = await client.session.list({});
|
|
21178
|
+
const allSessions = (list.data || []);
|
|
21179
|
+
const match = allSessions.find((s) => s.id === sessionId);
|
|
21180
|
+
if (!match) {
|
|
21181
|
+
await answerAndEdit(ctx, "Session not found. It may have been deleted.");
|
|
21182
|
+
return;
|
|
21183
|
+
}
|
|
21184
|
+
// Delete the button message
|
|
21185
|
+
try {
|
|
21186
|
+
await ctx.deleteMessage();
|
|
21187
|
+
}
|
|
21188
|
+
catch {
|
|
21189
|
+
// Ignore if already deleted
|
|
21190
|
+
}
|
|
21191
|
+
await ctx.answerCbQuery();
|
|
21192
|
+
// Adopt: add to known sessions and switch to it
|
|
21193
|
+
knownSessionIds.add(match.id);
|
|
21194
|
+
chatSessions.set(chatId, match.id);
|
|
21195
|
+
saveSessions();
|
|
21196
|
+
console.log(`[Telegram] Adopted and switched chat ${chatId} to session ${match.id}`);
|
|
21197
|
+
await ctx.reply(`Adopted and switched to session: ${match.title}`);
|
|
21198
|
+
}
|
|
21199
|
+
catch (err) {
|
|
21200
|
+
console.error("[Telegram] Error adopting session:", err);
|
|
21201
|
+
await answerAndEdit(ctx, "Failed to adopt session.");
|
|
21202
|
+
}
|
|
21203
|
+
});
|
|
21065
21204
|
// Handle /verbose command - toggle verbose mode for this chat
|
|
21066
21205
|
// Usage: /verbose, /verbose on, /verbose off
|
|
21067
21206
|
bot.command("verbose", (ctx) => {
|
|
@@ -21084,9 +21223,7 @@ async function startTelegram(options) {
|
|
|
21084
21223
|
saveSessions();
|
|
21085
21224
|
const enabled = chatVerboseMode.has(chatId);
|
|
21086
21225
|
console.log(`[Telegram] Verbose mode ${enabled ? "enabled" : "disabled"} for chat ${chatId}`);
|
|
21087
|
-
ctx.
|
|
21088
|
-
? "Verbose mode enabled. Responses will include thinking and tool calls."
|
|
21089
|
-
: "Verbose mode disabled. Responses will only show the assistant's text.");
|
|
21226
|
+
updatePinnedStatus(chatId, ctx.chat.id);
|
|
21090
21227
|
});
|
|
21091
21228
|
/**
|
|
21092
21229
|
* Fetch and search available models from connected providers.
|
|
@@ -21186,7 +21323,15 @@ async function startTelegram(options) {
|
|
|
21186
21323
|
const value = `${selection.providerID}/${selection.modelID}`;
|
|
21187
21324
|
chatModelOverride.set(chatId, value);
|
|
21188
21325
|
saveSessions();
|
|
21189
|
-
|
|
21326
|
+
try {
|
|
21327
|
+
await ctx.answerCbQuery();
|
|
21328
|
+
}
|
|
21329
|
+
catch { /* ignore */ }
|
|
21330
|
+
try {
|
|
21331
|
+
await ctx.deleteMessage();
|
|
21332
|
+
}
|
|
21333
|
+
catch { /* ignore */ }
|
|
21334
|
+
updatePinnedStatus(chatId, Number(chatId));
|
|
21190
21335
|
});
|
|
21191
21336
|
bot.action("model_default", async (ctx) => {
|
|
21192
21337
|
const chatId = getChatIdFromContext(ctx);
|
|
@@ -21194,7 +21339,144 @@ async function startTelegram(options) {
|
|
|
21194
21339
|
return;
|
|
21195
21340
|
chatModelOverride.delete(chatId);
|
|
21196
21341
|
saveSessions();
|
|
21197
|
-
|
|
21342
|
+
try {
|
|
21343
|
+
await ctx.answerCbQuery();
|
|
21344
|
+
}
|
|
21345
|
+
catch { /* ignore */ }
|
|
21346
|
+
try {
|
|
21347
|
+
await ctx.deleteMessage();
|
|
21348
|
+
}
|
|
21349
|
+
catch { /* ignore */ }
|
|
21350
|
+
updatePinnedStatus(chatId, Number(chatId));
|
|
21351
|
+
});
|
|
21352
|
+
// --- Agent switching and pinned status message ---
|
|
21353
|
+
function getActiveAgent(chatId) {
|
|
21354
|
+
return chatAgentOverride.get(chatId) || "build";
|
|
21355
|
+
}
|
|
21356
|
+
function getActiveModelDisplay(chatId) {
|
|
21357
|
+
const m = chatModelOverride.get(chatId) || model;
|
|
21358
|
+
if (!m)
|
|
21359
|
+
return "default";
|
|
21360
|
+
const parts = m.split("/");
|
|
21361
|
+
return parts.length > 1 ? parts.slice(1).join("/") : m;
|
|
21362
|
+
}
|
|
21363
|
+
function buildStatusText(chatId) {
|
|
21364
|
+
const agent = getActiveAgent(chatId);
|
|
21365
|
+
const modelDisplay = getActiveModelDisplay(chatId);
|
|
21366
|
+
const verbose = chatVerboseMode.has(chatId) ? "on" : "off";
|
|
21367
|
+
return `Agent: *${agent}* | Model: ${modelDisplay} | Verbose: ${verbose}`;
|
|
21368
|
+
}
|
|
21369
|
+
function buildStatusKeyboard(chatId) {
|
|
21370
|
+
const active = getActiveAgent(chatId);
|
|
21371
|
+
// Show non-hidden agents as buttons, highlight the active one
|
|
21372
|
+
const buttons = availableAgents
|
|
21373
|
+
.filter((a) => a.mode === "primary" || a.mode === "subagent")
|
|
21374
|
+
.map((a) => ({
|
|
21375
|
+
text: a.name === active ? `[${a.name}]` : a.name,
|
|
21376
|
+
callback_data: `agent:${a.name}`,
|
|
21377
|
+
}));
|
|
21378
|
+
// Arrange in rows of 3
|
|
21379
|
+
const keyboard = [];
|
|
21380
|
+
for (let i = 0; i < buttons.length; i += 3) {
|
|
21381
|
+
keyboard.push(buttons.slice(i, i + 3));
|
|
21382
|
+
}
|
|
21383
|
+
return keyboard;
|
|
21384
|
+
}
|
|
21385
|
+
async function updatePinnedStatus(chatId, numericChatId) {
|
|
21386
|
+
const text = buildStatusText(chatId);
|
|
21387
|
+
// Delete the old status message to keep the chat clean
|
|
21388
|
+
const existingMsgId = chatPinnedStatusMsg.get(chatId);
|
|
21389
|
+
if (existingMsgId) {
|
|
21390
|
+
try {
|
|
21391
|
+
await bot.telegram.deleteMessage(numericChatId, existingMsgId);
|
|
21392
|
+
}
|
|
21393
|
+
catch {
|
|
21394
|
+
// Already deleted
|
|
21395
|
+
}
|
|
21396
|
+
chatPinnedStatusMsg.delete(chatId);
|
|
21397
|
+
}
|
|
21398
|
+
// Always send a fresh message and pin it. Editing in place doesn't
|
|
21399
|
+
// refresh the pinned bar on Android, so delete+send+pin is the only
|
|
21400
|
+
// reliable approach. The pinned message itself serves as the
|
|
21401
|
+
// confirmation — no separate reply is needed.
|
|
21402
|
+
try {
|
|
21403
|
+
const msg = await bot.telegram.sendMessage(numericChatId, text, {
|
|
21404
|
+
parse_mode: "Markdown",
|
|
21405
|
+
});
|
|
21406
|
+
const messageId = msg.message_id;
|
|
21407
|
+
if (messageId) {
|
|
21408
|
+
chatPinnedStatusMsg.set(chatId, messageId);
|
|
21409
|
+
saveSessions();
|
|
21410
|
+
try {
|
|
21411
|
+
await bot.telegram.pinChatMessage(numericChatId, messageId, {
|
|
21412
|
+
disable_notification: true,
|
|
21413
|
+
});
|
|
21414
|
+
}
|
|
21415
|
+
catch (pinErr) {
|
|
21416
|
+
console.warn("[Telegram] Failed to pin status message:", pinErr);
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
}
|
|
21420
|
+
catch (err) {
|
|
21421
|
+
console.warn("[Telegram] Failed to send status message:", err);
|
|
21422
|
+
}
|
|
21423
|
+
}
|
|
21424
|
+
// Handle /agent command
|
|
21425
|
+
bot.command("agent", async (ctx) => {
|
|
21426
|
+
const chatId = ctx.chat.id.toString();
|
|
21427
|
+
const args = (ctx.message?.text || "").replace(/^\/agent\s*/i, "").trim();
|
|
21428
|
+
if (!args) {
|
|
21429
|
+
// Show current agent and list available ones
|
|
21430
|
+
const current = getActiveAgent(chatId);
|
|
21431
|
+
let msg = `Current agent: *${current}*\n\nAvailable agents:\n`;
|
|
21432
|
+
for (const a of availableAgents) {
|
|
21433
|
+
const marker = a.name === current ? " (active)" : "";
|
|
21434
|
+
const desc = a.description ? ` -- ${truncate(a.description, 80)}` : "";
|
|
21435
|
+
msg += `- *${a.name}*${marker}${desc}\n`;
|
|
21436
|
+
}
|
|
21437
|
+
msg += "\nTap a button or use `/agent <name>` to switch.";
|
|
21438
|
+
const keyboard = buildStatusKeyboard(chatId);
|
|
21439
|
+
await ctx.reply(msg, {
|
|
21440
|
+
parse_mode: "Markdown",
|
|
21441
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
21442
|
+
});
|
|
21443
|
+
return;
|
|
21444
|
+
}
|
|
21445
|
+
// Direct switch by name
|
|
21446
|
+
const target = args.toLowerCase();
|
|
21447
|
+
const match = availableAgents.find((a) => a.name === target);
|
|
21448
|
+
if (!match) {
|
|
21449
|
+
await ctx.reply(`Unknown agent "${args}". Available: ${availableAgents.map((a) => a.name).join(", ")}`);
|
|
21450
|
+
return;
|
|
21451
|
+
}
|
|
21452
|
+
chatAgentOverride.set(chatId, match.name);
|
|
21453
|
+
saveSessions();
|
|
21454
|
+
await updatePinnedStatus(chatId, ctx.chat.id);
|
|
21455
|
+
});
|
|
21456
|
+
// Handle agent switch via inline button
|
|
21457
|
+
bot.action(/^agent:(.+)$/, async (ctx) => {
|
|
21458
|
+
const chatId = getChatIdFromContext(ctx);
|
|
21459
|
+
if (!chatId)
|
|
21460
|
+
return;
|
|
21461
|
+
const agentName = ctx.match?.[1];
|
|
21462
|
+
if (!agentName)
|
|
21463
|
+
return;
|
|
21464
|
+
const match = availableAgents.find((a) => a.name === agentName);
|
|
21465
|
+
if (!match) {
|
|
21466
|
+
await answerAndEdit(ctx, `Unknown agent: ${agentName}`);
|
|
21467
|
+
return;
|
|
21468
|
+
}
|
|
21469
|
+
chatAgentOverride.set(chatId, match.name);
|
|
21470
|
+
saveSessions();
|
|
21471
|
+
try {
|
|
21472
|
+
await ctx.answerCbQuery();
|
|
21473
|
+
}
|
|
21474
|
+
catch { /* ignore */ }
|
|
21475
|
+
try {
|
|
21476
|
+
await ctx.deleteMessage();
|
|
21477
|
+
}
|
|
21478
|
+
catch { /* ignore */ }
|
|
21479
|
+
await updatePinnedStatus(chatId, Number(chatId));
|
|
21198
21480
|
});
|
|
21199
21481
|
// Handle question answer callback (from OpenCode question.asked events)
|
|
21200
21482
|
bot.action(/^qa:([^:]+):(\d+):(\d+)$/, async (ctx) => {
|