@grinev/opencode-telegram-bot 0.14.0 → 0.15.0
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/.env.example +16 -0
- package/README.md +44 -25
- package/dist/agent/manager.js +34 -6
- package/dist/agent/types.js +7 -1
- package/dist/bot/commands/commands.js +2 -2
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/new.js +3 -2
- package/dist/bot/commands/projects.js +3 -2
- package/dist/bot/commands/sessions.js +3 -0
- package/dist/bot/commands/status.js +5 -2
- package/dist/bot/commands/tts.js +13 -0
- package/dist/bot/handlers/agent.js +3 -4
- package/dist/bot/handlers/model.js +3 -2
- package/dist/bot/handlers/prompt.js +22 -5
- package/dist/bot/handlers/variant.js +3 -2
- package/dist/bot/index.js +40 -10
- package/dist/bot/message-patterns.js +2 -2
- package/dist/bot/streaming/tool-call-streamer.js +31 -24
- package/dist/bot/utils/finalize-assistant-response.js +6 -3
- package/dist/bot/utils/keyboard.js +6 -6
- package/dist/bot/utils/send-tts-response.js +37 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +82 -4
- package/dist/bot/utils/telegram-text.js +4 -2
- package/dist/config.js +8 -0
- package/dist/i18n/de.js +17 -9
- package/dist/i18n/en.js +17 -9
- package/dist/i18n/es.js +17 -9
- package/dist/i18n/fr.js +17 -9
- package/dist/i18n/ru.js +17 -9
- package/dist/i18n/zh.js +17 -9
- package/dist/pinned/manager.js +164 -31
- package/dist/settings/manager.js +7 -0
- package/dist/summary/formatter.js +10 -1
- package/dist/tts/client.js +59 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -46,6 +46,14 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
46
46
|
# Maximum number of scheduled tasks allowed at once (default: 10)
|
|
47
47
|
# TASK_LIMIT=10
|
|
48
48
|
|
|
49
|
+
# Stream update throttle in milliseconds for assistant/tool message edits (default: 500)
|
|
50
|
+
# Higher value = fewer Telegram edit requests, lower value = more real-time updates
|
|
51
|
+
# RESPONSE_STREAM_THROTTLE_MS=500
|
|
52
|
+
|
|
53
|
+
# Maximum displayed length for bash tool commands in Telegram summaries (default: 128)
|
|
54
|
+
# Longer commands are truncated with "..."
|
|
55
|
+
# BASH_TOOL_DISPLAY_MAX_LENGTH=128
|
|
56
|
+
|
|
49
57
|
# Bot locale: supported locale code (default: en)
|
|
50
58
|
# Supported locales: en, de, es, fr, ru, zh
|
|
51
59
|
# BOT_LOCALE=en
|
|
@@ -73,3 +81,11 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
73
81
|
# STT_API_KEY=
|
|
74
82
|
# STT_MODEL=
|
|
75
83
|
# STT_LANGUAGE=
|
|
84
|
+
|
|
85
|
+
# Text-to-Speech credentials (optional)
|
|
86
|
+
# TTS reply behavior is controlled globally with /tts and persisted in settings.json.
|
|
87
|
+
# Set both TTS_API_URL and TTS_API_KEY to enable audio replies.
|
|
88
|
+
# TTS_API_URL=
|
|
89
|
+
# TTS_API_KEY=
|
|
90
|
+
# TTS_MODEL=gpt-4o-mini-tts
|
|
91
|
+
# TTS_VOICE=alloy
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
31
31
|
- **Subagent activity** — watch live subagent progress in chat, including the current task, agent, model, and active tool step
|
|
32
32
|
- **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
|
|
33
33
|
- **Interactive Q&A** — answer agent questions and approve permissions via inline buttons
|
|
34
|
-
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API,
|
|
34
|
+
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, and optionally enable spoken replies with `/tts`
|
|
35
35
|
- **File attachments** — send images, PDF documents, and any text-based files to OpenCode (code, logs, configs etc.)
|
|
36
36
|
- **Scheduled tasks** — schedule prompts to run later or on a recurring interval; see [Scheduled Tasks](#scheduled-tasks)
|
|
37
37
|
- **Context control** — compact context when it gets too large, right from the chat
|
|
@@ -109,6 +109,7 @@ opencode-telegram config
|
|
|
109
109
|
| `/abort` | Abort the current task |
|
|
110
110
|
| `/sessions` | Browse and switch between recent sessions |
|
|
111
111
|
| `/projects` | Switch between OpenCode projects |
|
|
112
|
+
| `/tts` | Toggle audio replies |
|
|
112
113
|
| `/rename` | Rename the current session |
|
|
113
114
|
| `/commands` | Browse and run custom commands |
|
|
114
115
|
| `/task` | Create a scheduled task |
|
|
@@ -147,30 +148,37 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
147
148
|
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
|
|
148
149
|
- **Linux:** `~/.config/opencode-telegram-bot/.env`
|
|
149
150
|
|
|
150
|
-
| Variable
|
|
151
|
-
|
|
|
152
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
153
|
-
| `TELEGRAM_ALLOWED_USER_ID`
|
|
154
|
-
| `TELEGRAM_PROXY_URL`
|
|
155
|
-
| `OPENCODE_API_URL`
|
|
156
|
-
| `OPENCODE_SERVER_USERNAME`
|
|
157
|
-
| `OPENCODE_SERVER_PASSWORD`
|
|
158
|
-
| `OPENCODE_MODEL_PROVIDER`
|
|
159
|
-
| `OPENCODE_MODEL_ID`
|
|
160
|
-
| `BOT_LOCALE`
|
|
161
|
-
| `SESSIONS_LIST_LIMIT`
|
|
162
|
-
| `PROJECTS_LIST_LIMIT`
|
|
163
|
-
| `COMMANDS_LIST_LIMIT`
|
|
164
|
-
| `TASK_LIMIT`
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
151
|
+
| Variable | Description | Required | Default |
|
|
152
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ | :------: | ------------------------ |
|
|
153
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | — |
|
|
154
|
+
| `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | — |
|
|
155
|
+
| `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | — |
|
|
156
|
+
| `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
|
|
157
|
+
| `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
|
|
158
|
+
| `OPENCODE_SERVER_PASSWORD` | Server auth password | No | — |
|
|
159
|
+
| `OPENCODE_MODEL_PROVIDER` | Default model provider | Yes | `opencode` |
|
|
160
|
+
| `OPENCODE_MODEL_ID` | Default model ID | Yes | `big-pickle` |
|
|
161
|
+
| `BOT_LOCALE` | Bot UI language (supported locale code, e.g. `en`, `de`, `es`, `fr`, `ru`, `zh`) | No | `en` |
|
|
162
|
+
| `SESSIONS_LIST_LIMIT` | Sessions per page in `/sessions` | No | `10` |
|
|
163
|
+
| `PROJECTS_LIST_LIMIT` | Projects per page in `/projects` | No | `10` |
|
|
164
|
+
| `COMMANDS_LIST_LIMIT` | Commands per page in `/commands` | No | `10` |
|
|
165
|
+
| `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
|
|
166
|
+
| `BASH_TOOL_DISPLAY_MAX_LENGTH` | Maximum displayed length for `bash` tool commands in Telegram summaries; longer commands are truncated | No | `128` |
|
|
167
|
+
| `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
|
|
168
|
+
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
169
|
+
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
170
|
+
| `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
|
|
171
|
+
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
172
|
+
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
173
|
+
| `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
|
|
174
|
+
| `STT_API_KEY` | API key for your STT provider | No | — |
|
|
175
|
+
| `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
|
|
176
|
+
| `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
|
|
177
|
+
| `TTS_API_URL` | TTS API base URL | No | — |
|
|
178
|
+
| `TTS_API_KEY` | TTS API key | No | — |
|
|
179
|
+
| `TTS_MODEL` | TTS model name passed to `/audio/speech` | No | `gpt-4o-mini-tts` |
|
|
180
|
+
| `TTS_VOICE` | OpenAI-compatible TTS voice name | No | `alloy` |
|
|
181
|
+
| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
|
|
174
182
|
|
|
175
183
|
> **Keep your `.env` file private.** It contains your bot token. Never commit it to version control.
|
|
176
184
|
|
|
@@ -183,6 +191,17 @@ If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
|
|
|
183
191
|
3. Show recognized text in chat
|
|
184
192
|
4. Send the recognized text to OpenCode as a normal prompt
|
|
185
193
|
|
|
194
|
+
If TTS credentials are configured, you can toggle spoken replies globally with `/tts`. The preference is stored in `settings.json` and persists across restarts.
|
|
195
|
+
|
|
196
|
+
TTS configuration example:
|
|
197
|
+
|
|
198
|
+
```env
|
|
199
|
+
TTS_API_URL=https://api.openai.com/v1
|
|
200
|
+
TTS_API_KEY=your-tts-api-key
|
|
201
|
+
TTS_MODEL=gpt-4o-mini-tts
|
|
202
|
+
TTS_VOICE=alloy
|
|
203
|
+
```
|
|
204
|
+
|
|
186
205
|
Supported provider examples (Whisper-compatible):
|
|
187
206
|
|
|
188
207
|
- **OpenAI**
|
package/dist/agent/manager.js
CHANGED
|
@@ -29,6 +29,31 @@ export async function getAvailableAgents() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const DEFAULT_AGENT = "build";
|
|
32
|
+
function pickFallbackAgent(agents) {
|
|
33
|
+
const defaultAgent = agents.find((agent) => agent.name === DEFAULT_AGENT);
|
|
34
|
+
if (defaultAgent) {
|
|
35
|
+
return defaultAgent.name;
|
|
36
|
+
}
|
|
37
|
+
return agents[0]?.name ?? DEFAULT_AGENT;
|
|
38
|
+
}
|
|
39
|
+
export async function resolveProjectAgent(preferredAgent) {
|
|
40
|
+
const requestedAgent = preferredAgent ?? getCurrentAgent() ?? DEFAULT_AGENT;
|
|
41
|
+
const project = getCurrentProject();
|
|
42
|
+
if (!project) {
|
|
43
|
+
return requestedAgent;
|
|
44
|
+
}
|
|
45
|
+
const agents = await getAvailableAgents();
|
|
46
|
+
if (agents.length === 0) {
|
|
47
|
+
return requestedAgent;
|
|
48
|
+
}
|
|
49
|
+
if (agents.some((agent) => agent.name === requestedAgent)) {
|
|
50
|
+
return requestedAgent;
|
|
51
|
+
}
|
|
52
|
+
const fallbackAgent = pickFallbackAgent(agents);
|
|
53
|
+
logger.warn(`[AgentManager] Agent "${requestedAgent}" is not available for project ${project.worktree}. Falling back to "${fallbackAgent}".`);
|
|
54
|
+
setCurrentAgent(fallbackAgent);
|
|
55
|
+
return fallbackAgent;
|
|
56
|
+
}
|
|
32
57
|
/**
|
|
33
58
|
* Get current agent from last session message or settings.
|
|
34
59
|
* Falls back to "build" if nothing is stored.
|
|
@@ -38,10 +63,13 @@ export async function fetchCurrentAgent() {
|
|
|
38
63
|
const storedAgent = getCurrentAgent();
|
|
39
64
|
const session = getCurrentSession();
|
|
40
65
|
const project = getCurrentProject();
|
|
41
|
-
if (!
|
|
42
|
-
// No active
|
|
66
|
+
if (!project) {
|
|
67
|
+
// No active project, return stored agent from settings
|
|
43
68
|
return storedAgent ?? DEFAULT_AGENT;
|
|
44
69
|
}
|
|
70
|
+
if (!session) {
|
|
71
|
+
return resolveProjectAgent(storedAgent ?? DEFAULT_AGENT);
|
|
72
|
+
}
|
|
45
73
|
try {
|
|
46
74
|
const { data: messages, error } = await opencodeClient.session.messages({
|
|
47
75
|
sessionID: session.id,
|
|
@@ -50,7 +78,7 @@ export async function fetchCurrentAgent() {
|
|
|
50
78
|
});
|
|
51
79
|
if (error || !messages || messages.length === 0) {
|
|
52
80
|
logger.debug("[AgentManager] No messages found, using stored agent");
|
|
53
|
-
return storedAgent ?? DEFAULT_AGENT;
|
|
81
|
+
return resolveProjectAgent(storedAgent ?? DEFAULT_AGENT);
|
|
54
82
|
}
|
|
55
83
|
const lastAgent = messages[0].info.agent;
|
|
56
84
|
logger.debug(`[AgentManager] Current agent from session: ${lastAgent}`);
|
|
@@ -58,17 +86,17 @@ export async function fetchCurrentAgent() {
|
|
|
58
86
|
// Session messages may contain stale agent until next prompt is sent.
|
|
59
87
|
if (storedAgent && lastAgent !== storedAgent) {
|
|
60
88
|
logger.debug(`[AgentManager] Using stored agent "${storedAgent}" instead of session agent "${lastAgent}"`);
|
|
61
|
-
return storedAgent;
|
|
89
|
+
return resolveProjectAgent(storedAgent);
|
|
62
90
|
}
|
|
63
91
|
// No stored agent yet: sync from session history
|
|
64
92
|
if (lastAgent && lastAgent !== storedAgent) {
|
|
65
93
|
setCurrentAgent(lastAgent);
|
|
66
94
|
}
|
|
67
|
-
return lastAgent || storedAgent || DEFAULT_AGENT;
|
|
95
|
+
return resolveProjectAgent(lastAgent || storedAgent || DEFAULT_AGENT);
|
|
68
96
|
}
|
|
69
97
|
catch (err) {
|
|
70
98
|
logger.error("[AgentManager] Error fetching current agent:", err);
|
|
71
|
-
return storedAgent ?? DEFAULT_AGENT;
|
|
99
|
+
return resolveProjectAgent(storedAgent ?? DEFAULT_AGENT);
|
|
72
100
|
}
|
|
73
101
|
}
|
|
74
102
|
/**
|
package/dist/agent/types.js
CHANGED
|
@@ -22,5 +22,11 @@ export function getAgentEmoji(agentName) {
|
|
|
22
22
|
export function getAgentDisplayName(agentName) {
|
|
23
23
|
const emoji = getAgentEmoji(agentName);
|
|
24
24
|
const capitalizedName = agentName.charAt(0).toUpperCase() + agentName.slice(1);
|
|
25
|
-
return `${emoji} ${capitalizedName}
|
|
25
|
+
return `${emoji} ${capitalizedName}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get reply keyboard label for agent button.
|
|
29
|
+
*/
|
|
30
|
+
export function getAgentButtonLabel(agentName) {
|
|
31
|
+
return `${getAgentDisplayName(agentName)} Agent`;
|
|
26
32
|
}
|
|
@@ -5,7 +5,7 @@ import { clearSession, getCurrentSession, setCurrentSession, } from "../../sessi
|
|
|
5
5
|
import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
|
|
6
6
|
import { interactionManager } from "../../interaction/manager.js";
|
|
7
7
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
8
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
8
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
9
9
|
import { getStoredModel } from "../../model/manager.js";
|
|
10
10
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
@@ -279,7 +279,7 @@ async function executeCommand(ctx, deps, params) {
|
|
|
279
279
|
await ctx.reply(t("bot.session_busy"));
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
|
-
const currentAgent = getStoredAgent();
|
|
282
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
283
283
|
const storedModel = getStoredModel();
|
|
284
284
|
const model = storedModel.providerID && storedModel.modelID
|
|
285
285
|
? `${storedModel.providerID}/${storedModel.modelID}`
|
|
@@ -8,6 +8,7 @@ const COMMAND_DEFINITIONS = [
|
|
|
8
8
|
{ command: "new", descriptionKey: "cmd.description.new" },
|
|
9
9
|
{ command: "abort", descriptionKey: "cmd.description.stop" },
|
|
10
10
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
|
+
{ command: "tts", descriptionKey: "cmd.description.tts" },
|
|
11
12
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
12
13
|
{ command: "task", descriptionKey: "cmd.description.task" },
|
|
13
14
|
{ command: "tasklist", descriptionKey: "cmd.description.tasklist" },
|
package/dist/bot/commands/new.js
CHANGED
|
@@ -6,7 +6,7 @@ import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
|
6
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
7
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
8
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
9
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
10
10
|
import { getStoredModel } from "../../model/manager.js";
|
|
11
11
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
12
12
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
@@ -54,10 +54,11 @@ export async function newCommand(ctx) {
|
|
|
54
54
|
logger.error("[Bot] Error creating pinned message:", err);
|
|
55
55
|
}
|
|
56
56
|
// Get current state for keyboard
|
|
57
|
-
const currentAgent = getStoredAgent();
|
|
57
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
58
58
|
const currentModel = getStoredModel();
|
|
59
59
|
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
60
60
|
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
61
|
+
keyboardManager.updateAgent(currentAgent);
|
|
61
62
|
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
|
|
62
63
|
await ctx.reply(t("new.created", { title: session.title }), {
|
|
63
64
|
reply_markup: keyboard,
|
|
@@ -6,7 +6,7 @@ import { clearSession } from "../../session/manager.js";
|
|
|
6
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
7
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
8
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
9
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
10
10
|
import { getStoredModel } from "../../model/manager.js";
|
|
11
11
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
12
12
|
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
@@ -201,10 +201,11 @@ export async function handleProjectSelect(ctx) {
|
|
|
201
201
|
// Reset context to 0 (no session selected) with current model's limit
|
|
202
202
|
keyboardManager.updateContext(0, contextLimit);
|
|
203
203
|
// Get current state for keyboard (with context = 0)
|
|
204
|
-
const currentAgent = getStoredAgent();
|
|
204
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
205
205
|
const currentModel = getStoredModel();
|
|
206
206
|
const contextInfo = { tokensUsed: 0, tokensLimit: contextLimit };
|
|
207
207
|
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
208
|
+
keyboardManager.updateAgent(currentAgent);
|
|
208
209
|
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo, variantName);
|
|
209
210
|
const projectName = selectedProject.name || selectedProject.worktree;
|
|
210
211
|
await ctx.answerCallbackQuery();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { opencodeClient } from "../../opencode/client.js";
|
|
3
|
+
import { resolveProjectAgent } from "../../agent/manager.js";
|
|
3
4
|
import { setCurrentSession } from "../../session/manager.js";
|
|
4
5
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
6
|
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
@@ -214,6 +215,8 @@ export async function handleSessionSelect(ctx) {
|
|
|
214
215
|
}
|
|
215
216
|
if (ctx.chat) {
|
|
216
217
|
const chatId = ctx.chat.id;
|
|
218
|
+
const currentAgent = await resolveProjectAgent();
|
|
219
|
+
keyboardManager.updateAgent(currentAgent);
|
|
217
220
|
// Update keyboard with loaded context (callback executes async via setImmediate, so update manually)
|
|
218
221
|
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
219
222
|
if (contextInfo) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { opencodeClient } from "../../opencode/client.js";
|
|
2
2
|
import { getCurrentSession } from "../../session/manager.js";
|
|
3
|
-
import { getCurrentProject } from "../../settings/manager.js";
|
|
3
|
+
import { getCurrentProject, isTtsEnabled } from "../../settings/manager.js";
|
|
4
4
|
import { fetchCurrentAgent } from "../../agent/manager.js";
|
|
5
5
|
import { getAgentDisplayName } from "../../agent/types.js";
|
|
6
6
|
import { fetchCurrentModel } from "../../model/manager.js";
|
|
@@ -22,6 +22,9 @@ export async function statusCommand(ctx) {
|
|
|
22
22
|
if (data.version) {
|
|
23
23
|
message += `${t("status.line.version", { version: data.version })}\n`;
|
|
24
24
|
}
|
|
25
|
+
message += `${t("status.line.tts", {
|
|
26
|
+
tts: isTtsEnabled() ? t("status.tts.on") : t("status.tts.off"),
|
|
27
|
+
})}\n`;
|
|
25
28
|
// Add process management information
|
|
26
29
|
if (processManager.isRunning()) {
|
|
27
30
|
const uptime = processManager.getUptime();
|
|
@@ -33,7 +36,7 @@ export async function statusCommand(ctx) {
|
|
|
33
36
|
else {
|
|
34
37
|
message += `${t("status.line.managed_no")}\n`;
|
|
35
38
|
}
|
|
36
|
-
// Add agent
|
|
39
|
+
// Add agent information
|
|
37
40
|
const currentAgent = await fetchCurrentAgent();
|
|
38
41
|
const agentDisplay = currentAgent
|
|
39
42
|
? getAgentDisplayName(currentAgent)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isTtsConfigured } from "../../tts/client.js";
|
|
2
|
+
import { isTtsEnabled, setTtsEnabled } from "../../settings/manager.js";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
export async function ttsCommand(ctx) {
|
|
5
|
+
const enabled = !isTtsEnabled();
|
|
6
|
+
if (enabled && !isTtsConfigured()) {
|
|
7
|
+
await ctx.reply(t("tts.not_configured"));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
setTtsEnabled(enabled);
|
|
11
|
+
const message = enabled ? t("tts.enabled") : t("tts.disabled");
|
|
12
|
+
await ctx.reply(message);
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { selectAgent, getAvailableAgents, fetchCurrentAgent } from "../../agent/manager.js";
|
|
3
|
-
import { getAgentDisplayName
|
|
3
|
+
import { getAgentDisplayName } from "../../agent/types.js";
|
|
4
4
|
import { getStoredModel } from "../../model/manager.js";
|
|
5
5
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
6
6
|
import { logger } from "../../utils/logger.js";
|
|
@@ -81,11 +81,10 @@ export async function buildAgentSelectionMenu(currentAgent) {
|
|
|
81
81
|
}
|
|
82
82
|
// Add button for each agent
|
|
83
83
|
agents.forEach((agent) => {
|
|
84
|
-
const emoji = getAgentEmoji(agent.name);
|
|
85
84
|
const isActive = agent.name === currentAgent;
|
|
86
85
|
const label = isActive
|
|
87
|
-
? `✅ ${
|
|
88
|
-
:
|
|
86
|
+
? `✅ ${getAgentDisplayName(agent.name)}`
|
|
87
|
+
: getAgentDisplayName(agent.name);
|
|
89
88
|
keyboard.text(label, `agent:${agent.name}`).row();
|
|
90
89
|
});
|
|
91
90
|
return keyboard;
|
|
@@ -4,7 +4,7 @@ import { formatModelForDisplay } from "../../model/types.js";
|
|
|
4
4
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
6
6
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
7
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
7
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
8
8
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
9
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
10
|
import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
|
|
@@ -61,11 +61,12 @@ export async function handleModelSelect(ctx) {
|
|
|
61
61
|
// Refresh context limit for new model
|
|
62
62
|
await pinnedMessageManager.refreshContextLimit();
|
|
63
63
|
// Update Reply Keyboard with new model and context
|
|
64
|
-
const currentAgent = getStoredAgent();
|
|
64
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
65
65
|
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
66
66
|
(pinnedMessageManager.getContextLimit() > 0
|
|
67
67
|
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
68
68
|
: null);
|
|
69
|
+
keyboardManager.updateAgent(currentAgent);
|
|
69
70
|
if (contextInfo) {
|
|
70
71
|
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
71
72
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { opencodeClient } from "../../opencode/client.js";
|
|
2
2
|
import { clearSession, getCurrentSession, setCurrentSession } from "../../session/manager.js";
|
|
3
3
|
import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
|
|
4
|
-
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
4
|
+
import { getCurrentProject, isTtsEnabled } from "../../settings/manager.js";
|
|
5
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
6
6
|
import { getStoredModel } from "../../model/manager.js";
|
|
7
7
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
8
8
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
@@ -20,12 +20,24 @@ import { foregroundSessionState } from "../../scheduled-task/foreground-state.js
|
|
|
20
20
|
/** Module-level references for async callbacks that don't have ctx. */
|
|
21
21
|
let botInstance = null;
|
|
22
22
|
let chatIdInstance = null;
|
|
23
|
+
const promptResponseModes = new Map();
|
|
23
24
|
export function getPromptBotInstance() {
|
|
24
25
|
return botInstance;
|
|
25
26
|
}
|
|
26
27
|
export function getPromptChatId() {
|
|
27
28
|
return chatIdInstance;
|
|
28
29
|
}
|
|
30
|
+
export function setPromptResponseMode(sessionId, responseMode) {
|
|
31
|
+
promptResponseModes.set(sessionId, responseMode);
|
|
32
|
+
}
|
|
33
|
+
export function clearPromptResponseMode(sessionId) {
|
|
34
|
+
promptResponseModes.delete(sessionId);
|
|
35
|
+
}
|
|
36
|
+
export function consumePromptResponseMode(sessionId) {
|
|
37
|
+
const responseMode = promptResponseModes.get(sessionId) ?? null;
|
|
38
|
+
promptResponseModes.delete(sessionId);
|
|
39
|
+
return responseMode;
|
|
40
|
+
}
|
|
29
41
|
async function isSessionBusy(sessionId, directory) {
|
|
30
42
|
try {
|
|
31
43
|
const { data, error } = await opencodeClient.session.status({ directory });
|
|
@@ -72,8 +84,9 @@ async function resetMismatchedSessionContext() {
|
|
|
72
84
|
* @param fileParts - Optional file parts (for photo/document attachments)
|
|
73
85
|
* @returns true if the prompt was dispatched, false if it was blocked/failed early.
|
|
74
86
|
*/
|
|
75
|
-
export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
87
|
+
export async function processUserPrompt(ctx, text, deps, fileParts = [], options = {}) {
|
|
76
88
|
const { bot, ensureEventSubscription } = deps;
|
|
89
|
+
const responseMode = options.responseMode ?? (isTtsEnabled() ? "text_and_tts" : "text_only");
|
|
77
90
|
const currentProject = getCurrentProject();
|
|
78
91
|
if (!currentProject) {
|
|
79
92
|
await ctx.reply(t("bot.project_not_selected"));
|
|
@@ -118,10 +131,11 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
|
118
131
|
catch (err) {
|
|
119
132
|
logger.error("[Bot] Error creating pinned message for new session:", err);
|
|
120
133
|
}
|
|
121
|
-
const currentAgent = getStoredAgent();
|
|
134
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
122
135
|
const currentModel = getStoredModel();
|
|
123
136
|
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
124
137
|
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
138
|
+
keyboardManager.updateAgent(currentAgent);
|
|
125
139
|
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
|
|
126
140
|
await ctx.reply(t("bot.session_created", { title: session.title }), {
|
|
127
141
|
reply_markup: keyboard,
|
|
@@ -149,7 +163,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
|
149
163
|
return false;
|
|
150
164
|
}
|
|
151
165
|
try {
|
|
152
|
-
const currentAgent = getStoredAgent();
|
|
166
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
153
167
|
const storedModel = getStoredModel();
|
|
154
168
|
// Build parts array with text and files
|
|
155
169
|
const parts = [];
|
|
@@ -195,6 +209,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
|
195
209
|
};
|
|
196
210
|
logger.info(`[Bot] Calling session.prompt (fire-and-forget) with agent=${currentAgent}, fileCount=${fileParts.length}...`);
|
|
197
211
|
foregroundSessionState.markBusy(currentSession.id);
|
|
212
|
+
setPromptResponseMode(currentSession.id, responseMode);
|
|
198
213
|
// CRITICAL: DO NOT wait for session.prompt to complete.
|
|
199
214
|
// If we wait, the handler will not finish and grammY will not call getUpdates,
|
|
200
215
|
// which blocks receiving button callback_query updates.
|
|
@@ -205,6 +220,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
|
205
220
|
onSuccess: ({ error }) => {
|
|
206
221
|
if (error) {
|
|
207
222
|
foregroundSessionState.markIdle(currentSession.id);
|
|
223
|
+
clearPromptResponseMode(currentSession.id);
|
|
208
224
|
const details = formatErrorDetails(error, 6000);
|
|
209
225
|
logger.error("[Bot] OpenCode API returned an error for session.prompt", promptErrorLogContext);
|
|
210
226
|
logger.error("[Bot] session.prompt error details:", details);
|
|
@@ -217,6 +233,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = []) {
|
|
|
217
233
|
},
|
|
218
234
|
onError: (error) => {
|
|
219
235
|
foregroundSessionState.markIdle(currentSession.id);
|
|
236
|
+
clearPromptResponseMode(currentSession.id);
|
|
220
237
|
const details = formatErrorDetails(error, 6000);
|
|
221
238
|
logger.error("[Bot] session.prompt background task failed", promptErrorLogContext);
|
|
222
239
|
logger.error("[Bot] session.prompt background failure details:", details);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { getAvailableVariants, getCurrentVariant, setCurrentVariant, formatVariantForDisplay, formatVariantForButton, } from "../../variant/manager.js";
|
|
3
3
|
import { getStoredModel } from "../../model/manager.js";
|
|
4
|
-
import { getStoredAgent } from "../../agent/manager.js";
|
|
4
|
+
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
6
6
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
7
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
@@ -47,11 +47,12 @@ export async function handleVariantSelect(ctx) {
|
|
|
47
47
|
keyboardManager.updateModel(updatedModel);
|
|
48
48
|
keyboardManager.updateVariant(variantId);
|
|
49
49
|
// Build keyboard with correct context info
|
|
50
|
-
const currentAgent = getStoredAgent();
|
|
50
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
51
51
|
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
52
52
|
(pinnedMessageManager.getContextLimit() > 0
|
|
53
53
|
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
54
54
|
: null);
|
|
55
|
+
keyboardManager.updateAgent(currentAgent);
|
|
55
56
|
if (contextInfo) {
|
|
56
57
|
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
57
58
|
}
|