@grinev/opencode-telegram-bot 0.14.1 → 0.16.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 +25 -0
- package/README.md +50 -26
- package/dist/agent/manager.js +34 -6
- package/dist/agent/types.js +7 -1
- package/dist/app/start-bot-app.js +6 -1
- package/dist/bot/assistant-run-state.js +60 -0
- package/dist/bot/commands/abort.js +2 -0
- package/dist/bot/commands/commands.js +12 -2
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/new.js +3 -2
- package/dist/bot/commands/open.js +314 -0
- package/dist/bot/commands/projects.js +5 -37
- package/dist/bot/commands/sessions.js +3 -0
- package/dist/bot/commands/start.js +2 -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/inline-menu.js +9 -1
- package/dist/bot/handlers/model.js +3 -2
- package/dist/bot/handlers/prompt.js +33 -5
- package/dist/bot/handlers/variant.js +3 -2
- package/dist/bot/index.js +179 -94
- package/dist/bot/message-patterns.js +2 -2
- package/dist/bot/streaming/response-streamer.js +26 -17
- package/dist/bot/streaming/tool-call-streamer.js +31 -24
- package/dist/bot/utils/assistant-rendering.js +117 -0
- package/dist/bot/utils/assistant-run-footer.js +9 -0
- package/dist/bot/utils/browser-roots.js +140 -0
- package/dist/bot/utils/file-tree.js +92 -0
- package/dist/bot/utils/finalize-assistant-response.js +18 -24
- 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 +3 -3
- package/dist/bot/utils/switch-project.js +48 -0
- package/dist/bot/utils/telegram-text.js +95 -1
- package/dist/cli.js +4 -0
- package/dist/config.js +10 -0
- package/dist/i18n/de.js +32 -9
- package/dist/i18n/en.js +32 -9
- package/dist/i18n/es.js +32 -9
- package/dist/i18n/fr.js +32 -9
- package/dist/i18n/ru.js +32 -9
- package/dist/i18n/zh.js +32 -9
- package/dist/index.js +2 -0
- package/dist/pinned/manager.js +66 -4
- package/dist/project/manager.js +2 -1
- package/dist/settings/manager.js +7 -0
- package/dist/summary/aggregator.js +17 -1
- package/dist/summary/formatter.js +12 -89
- package/dist/telegram/render/block-fallback.js +28 -0
- package/dist/telegram/render/block-parser.js +295 -0
- package/dist/telegram/render/block-renderer.js +457 -0
- package/dist/telegram/render/chunker.js +281 -0
- package/dist/telegram/render/inline-renderer.js +128 -0
- package/dist/telegram/render/markdown-normalizer.js +94 -0
- package/dist/telegram/render/pipeline.js +9 -0
- package/dist/telegram/render/types.js +1 -0
- package/dist/telegram/render/validator.js +160 -0
- package/dist/tts/client.js +59 -0
- package/dist/utils/logger.js +200 -73
- package/package.json +6 -2
package/.env.example
CHANGED
|
@@ -33,6 +33,11 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
33
33
|
# Use "debug" to see detailed diagnostic logs including all bot events
|
|
34
34
|
# LOG_LEVEL=info
|
|
35
35
|
|
|
36
|
+
# Number of log files to keep (default: 10)
|
|
37
|
+
# sources mode: keeps the latest N launch logs
|
|
38
|
+
# installed mode: keeps the latest N daily logs
|
|
39
|
+
# LOG_RETENTION=10
|
|
40
|
+
|
|
36
41
|
# Bot Configuration (optional)
|
|
37
42
|
# Maximum number of sessions shown in /sessions (default: 10)
|
|
38
43
|
# SESSIONS_LIST_LIMIT=10
|
|
@@ -50,6 +55,10 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
50
55
|
# Higher value = fewer Telegram edit requests, lower value = more real-time updates
|
|
51
56
|
# RESPONSE_STREAM_THROTTLE_MS=500
|
|
52
57
|
|
|
58
|
+
# Maximum displayed length for bash tool commands in Telegram summaries (default: 128)
|
|
59
|
+
# Longer commands are truncated with "..."
|
|
60
|
+
# BASH_TOOL_DISPLAY_MAX_LENGTH=128
|
|
61
|
+
|
|
53
62
|
# Bot locale: supported locale code (default: en)
|
|
54
63
|
# Supported locales: en, de, es, fr, ru, zh
|
|
55
64
|
# BOT_LOCALE=en
|
|
@@ -65,6 +74,14 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
65
74
|
# raw = show assistant replies as plain text
|
|
66
75
|
# MESSAGE_FORMAT_MODE=markdown
|
|
67
76
|
|
|
77
|
+
# Directory Browser Roots (optional)
|
|
78
|
+
# Comma-separated list of paths that /open is allowed to browse.
|
|
79
|
+
# Supports ~ for home directory. Defaults to ~ when not set.
|
|
80
|
+
# Examples:
|
|
81
|
+
# OPEN_BROWSER_ROOTS=~/projects
|
|
82
|
+
# OPEN_BROWSER_ROOTS=~/projects,~/work,/opt/repos
|
|
83
|
+
# OPEN_BROWSER_ROOTS=
|
|
84
|
+
|
|
68
85
|
# Code File Settings (optional)
|
|
69
86
|
# Maximum file size in KB to send as document (default: 100)
|
|
70
87
|
# CODE_FILE_MAX_SIZE_KB=100
|
|
@@ -77,3 +94,11 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
77
94
|
# STT_API_KEY=
|
|
78
95
|
# STT_MODEL=
|
|
79
96
|
# STT_LANGUAGE=
|
|
97
|
+
|
|
98
|
+
# Text-to-Speech credentials (optional)
|
|
99
|
+
# TTS reply behavior is controlled globally with /tts and persisted in settings.json.
|
|
100
|
+
# Set both TTS_API_URL and TTS_API_KEY to enable audio replies.
|
|
101
|
+
# TTS_API_URL=
|
|
102
|
+
# TTS_API_KEY=
|
|
103
|
+
# TTS_MODEL=gpt-4o-mini-tts
|
|
104
|
+
# TTS_VOICE=alloy
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://github.com/grinev/opencode-telegram-bot/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
+
[](https://x.com/grin_rus)
|
|
7
8
|
|
|
8
9
|
OpenCode Telegram Bot is a secure Telegram client for [OpenCode](https://opencode.ai) CLI that runs on your local machine.
|
|
9
10
|
|
|
@@ -31,7 +32,7 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
31
32
|
- **Subagent activity** — watch live subagent progress in chat, including the current task, agent, model, and active tool step
|
|
32
33
|
- **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
|
|
33
34
|
- **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,
|
|
35
|
+
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, and optionally enable spoken replies with `/tts`
|
|
35
36
|
- **File attachments** — send images, PDF documents, and any text-based files to OpenCode (code, logs, configs etc.)
|
|
36
37
|
- **Scheduled tasks** — schedule prompts to run later or on a recurring interval; see [Scheduled Tasks](#scheduled-tasks)
|
|
37
38
|
- **Context control** — compact context when it gets too large, right from the chat
|
|
@@ -109,6 +110,8 @@ opencode-telegram config
|
|
|
109
110
|
| `/abort` | Abort the current task |
|
|
110
111
|
| `/sessions` | Browse and switch between recent sessions |
|
|
111
112
|
| `/projects` | Switch between OpenCode projects |
|
|
113
|
+
| `/open` | Add a project by browsing directories |
|
|
114
|
+
| `/tts` | Toggle audio replies |
|
|
112
115
|
| `/rename` | Rename the current session |
|
|
113
116
|
| `/commands` | Browse and run custom commands |
|
|
114
117
|
| `/task` | Create a scheduled task |
|
|
@@ -147,34 +150,44 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
147
150
|
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
|
|
148
151
|
- **Linux:** `~/.config/opencode-telegram-bot/.env`
|
|
149
152
|
|
|
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
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
153
|
+
| Variable | Description | Required | Default |
|
|
154
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ | :------: | ------------------------ |
|
|
155
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | — |
|
|
156
|
+
| `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | — |
|
|
157
|
+
| `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | — |
|
|
158
|
+
| `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
|
|
159
|
+
| `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
|
|
160
|
+
| `OPENCODE_SERVER_PASSWORD` | Server auth password | No | — |
|
|
161
|
+
| `OPENCODE_MODEL_PROVIDER` | Default model provider | Yes | `opencode` |
|
|
162
|
+
| `OPENCODE_MODEL_ID` | Default model ID | Yes | `big-pickle` |
|
|
163
|
+
| `BOT_LOCALE` | Bot UI language (supported locale code, e.g. `en`, `de`, `es`, `fr`, `ru`, `zh`) | No | `en` |
|
|
164
|
+
| `SESSIONS_LIST_LIMIT` | Sessions per page in `/sessions` | No | `10` |
|
|
165
|
+
| `PROJECTS_LIST_LIMIT` | Projects per page in `/projects` | No | `10` |
|
|
166
|
+
| `OPEN_BROWSER_ROOTS` | Comma-separated paths `/open` is allowed to browse (supports `~`) | No | `~` (home directory) |
|
|
167
|
+
| `COMMANDS_LIST_LIMIT` | Commands per page in `/commands` | No | `10` |
|
|
168
|
+
| `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
|
|
169
|
+
| `BASH_TOOL_DISPLAY_MAX_LENGTH` | Maximum displayed length for `bash` tool commands in Telegram summaries; longer commands are truncated | No | `128` |
|
|
170
|
+
| `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
|
|
171
|
+
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
172
|
+
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
173
|
+
| `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
|
|
174
|
+
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
175
|
+
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
176
|
+
| `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
|
|
177
|
+
| `STT_API_KEY` | API key for your STT provider | No | — |
|
|
178
|
+
| `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
|
|
179
|
+
| `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
|
|
180
|
+
| `TTS_API_URL` | TTS API base URL | No | — |
|
|
181
|
+
| `TTS_API_KEY` | TTS API key | No | — |
|
|
182
|
+
| `TTS_MODEL` | TTS model name passed to `/audio/speech` | No | `gpt-4o-mini-tts` |
|
|
183
|
+
| `TTS_VOICE` | OpenAI-compatible TTS voice name | No | `alloy` |
|
|
184
|
+
| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
|
|
185
|
+
| `LOG_RETENTION` | Number of log files to keep: launch files in `sources`, daily files in `installed` | No | `10` |
|
|
175
186
|
|
|
176
187
|
> **Keep your `.env` file private.** It contains your bot token. Never commit it to version control.
|
|
177
188
|
|
|
189
|
+
Logs are written to `./logs` when running from sources and to the runtime config directory `logs/` folder in `installed` mode. Log rotation depends on runtime mode: `sources` creates one file per bot launch, while `installed` appends to one file per day. Old log files are removed according to `LOG_RETENTION`.
|
|
190
|
+
|
|
178
191
|
### Voice and Audio Transcription (Optional)
|
|
179
192
|
|
|
180
193
|
If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
|
|
@@ -184,6 +197,17 @@ If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
|
|
|
184
197
|
3. Show recognized text in chat
|
|
185
198
|
4. Send the recognized text to OpenCode as a normal prompt
|
|
186
199
|
|
|
200
|
+
If TTS credentials are configured, you can toggle spoken replies globally with `/tts`. The preference is stored in `settings.json` and persists across restarts.
|
|
201
|
+
|
|
202
|
+
TTS configuration example:
|
|
203
|
+
|
|
204
|
+
```env
|
|
205
|
+
TTS_API_URL=https://api.openai.com/v1
|
|
206
|
+
TTS_API_KEY=your-tts-api-key
|
|
207
|
+
TTS_MODEL=gpt-4o-mini-tts
|
|
208
|
+
TTS_VOICE=alloy
|
|
209
|
+
```
|
|
210
|
+
|
|
187
211
|
Supported provider examples (Whisper-compatible):
|
|
188
212
|
|
|
189
213
|
- **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
|
}
|
|
@@ -8,7 +8,7 @@ import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
|
|
|
8
8
|
import { reconcileStoredModelSelection } from "../model/manager.js";
|
|
9
9
|
import { getRuntimeMode } from "../runtime/mode.js";
|
|
10
10
|
import { getRuntimePaths } from "../runtime/paths.js";
|
|
11
|
-
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { getLogFilePath, initializeLogger, logger } from "../utils/logger.js";
|
|
12
12
|
async function getBotVersion() {
|
|
13
13
|
try {
|
|
14
14
|
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
@@ -22,11 +22,16 @@ async function getBotVersion() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export async function startBotApp() {
|
|
25
|
+
await initializeLogger();
|
|
25
26
|
const mode = getRuntimeMode();
|
|
26
27
|
const runtimePaths = getRuntimePaths();
|
|
27
28
|
const version = await getBotVersion();
|
|
29
|
+
const logFilePath = getLogFilePath();
|
|
28
30
|
logger.info(`Starting OpenCode Telegram Bot v${version}...`);
|
|
29
31
|
logger.info(`Config loaded from ${runtimePaths.envFilePath}`);
|
|
32
|
+
if (logFilePath) {
|
|
33
|
+
logger.info(`Logs are written to ${logFilePath}`);
|
|
34
|
+
}
|
|
30
35
|
logger.info(`Allowed User ID: ${config.telegram.allowedUserId}`);
|
|
31
36
|
logger.debug(`[Runtime] Application start mode: ${mode}`);
|
|
32
37
|
await loadSettings();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
class AssistantRunState {
|
|
3
|
+
runs = new Map();
|
|
4
|
+
startRun(sessionId, info) {
|
|
5
|
+
if (!sessionId) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
this.runs.set(sessionId, {
|
|
9
|
+
sessionId,
|
|
10
|
+
startedAt: info.startedAt,
|
|
11
|
+
configuredAgent: info.configuredAgent,
|
|
12
|
+
configuredProviderID: info.configuredProviderID,
|
|
13
|
+
configuredModelID: info.configuredModelID,
|
|
14
|
+
hasCompletedResponse: false,
|
|
15
|
+
});
|
|
16
|
+
logger.debug(`[AssistantRunState] Started run: session=${sessionId}, agent=${info.configuredAgent || "unknown"}, model=${info.configuredProviderID || "unknown"}/${info.configuredModelID || "unknown"}`);
|
|
17
|
+
}
|
|
18
|
+
markResponseCompleted(sessionId, info) {
|
|
19
|
+
const run = this.runs.get(sessionId);
|
|
20
|
+
if (!run) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
run.hasCompletedResponse = true;
|
|
24
|
+
if (info?.agent) {
|
|
25
|
+
run.actualAgent = info.agent;
|
|
26
|
+
}
|
|
27
|
+
if (info?.providerID) {
|
|
28
|
+
run.actualProviderID = info.providerID;
|
|
29
|
+
}
|
|
30
|
+
if (info?.modelID) {
|
|
31
|
+
run.actualModelID = info.modelID;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finishRun(sessionId, reason) {
|
|
35
|
+
const run = this.runs.get(sessionId) ?? null;
|
|
36
|
+
if (!run) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
this.runs.delete(sessionId);
|
|
40
|
+
logger.debug(`[AssistantRunState] Finished run: session=${sessionId}, reason=${reason}`);
|
|
41
|
+
return { ...run };
|
|
42
|
+
}
|
|
43
|
+
clearRun(sessionId, reason) {
|
|
44
|
+
if (!this.runs.delete(sessionId)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
logger.debug(`[AssistantRunState] Cleared run: session=${sessionId}, reason=${reason}`);
|
|
48
|
+
}
|
|
49
|
+
clearAll(reason) {
|
|
50
|
+
if (this.runs.size === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
logger.debug(`[AssistantRunState] Cleared all runs: count=${this.runs.size}, reason=${reason}`);
|
|
54
|
+
this.runs.clear();
|
|
55
|
+
}
|
|
56
|
+
__resetForTests() {
|
|
57
|
+
this.runs.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export const assistantRunState = new AssistantRunState();
|
|
@@ -6,6 +6,7 @@ import { summaryAggregator } from "../../summary/aggregator.js";
|
|
|
6
6
|
import { logger } from "../../utils/logger.js";
|
|
7
7
|
import { t } from "../../i18n/index.js";
|
|
8
8
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
9
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
9
10
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
11
|
function abortLocalStreaming() {
|
|
11
12
|
stopEventListening();
|
|
@@ -86,6 +87,7 @@ export async function abortCurrentOperation(ctx, options = {}) {
|
|
|
86
87
|
const finalStatus = await pollSessionStatus(currentSession.id, currentSession.directory, 5000);
|
|
87
88
|
if (finalStatus === "idle" || finalStatus === "not-found") {
|
|
88
89
|
foregroundSessionState.markIdle(currentSession.id);
|
|
90
|
+
assistantRunState.clearRun(currentSession.id, "abort_confirmed");
|
|
89
91
|
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
90
92
|
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.success"));
|
|
91
93
|
}
|
|
@@ -5,13 +5,14 @@ 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";
|
|
12
12
|
import { t } from "../../i18n/index.js";
|
|
13
13
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
14
14
|
import { config } from "../../config.js";
|
|
15
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
15
16
|
const COMMANDS_CALLBACK_PREFIX = "commands:";
|
|
16
17
|
const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
|
|
17
18
|
const COMMANDS_CALLBACK_PAGE_PREFIX = `${COMMANDS_CALLBACK_PREFIX}page:`;
|
|
@@ -236,6 +237,7 @@ async function ensureSessionForProject(ctx, projectDirectory) {
|
|
|
236
237
|
clearSession();
|
|
237
238
|
summaryAggregator.clear();
|
|
238
239
|
foregroundSessionState.clearAll("session_mismatch_reset");
|
|
240
|
+
assistantRunState.clearAll("session_mismatch_reset");
|
|
239
241
|
await ctx.reply(t("bot.session_reset_project_mismatch"));
|
|
240
242
|
currentSession = null;
|
|
241
243
|
}
|
|
@@ -279,12 +281,18 @@ async function executeCommand(ctx, deps, params) {
|
|
|
279
281
|
await ctx.reply(t("bot.session_busy"));
|
|
280
282
|
return;
|
|
281
283
|
}
|
|
282
|
-
const currentAgent = getStoredAgent();
|
|
284
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
283
285
|
const storedModel = getStoredModel();
|
|
284
286
|
const model = storedModel.providerID && storedModel.modelID
|
|
285
287
|
? `${storedModel.providerID}/${storedModel.modelID}`
|
|
286
288
|
: undefined;
|
|
287
289
|
foregroundSessionState.markBusy(session.id);
|
|
290
|
+
assistantRunState.startRun(session.id, {
|
|
291
|
+
startedAt: Date.now(),
|
|
292
|
+
configuredAgent: currentAgent,
|
|
293
|
+
configuredProviderID: storedModel.providerID,
|
|
294
|
+
configuredModelID: storedModel.modelID,
|
|
295
|
+
});
|
|
288
296
|
safeBackgroundTask({
|
|
289
297
|
taskName: "session.command",
|
|
290
298
|
task: () => opencodeClient.session.command({
|
|
@@ -299,6 +307,7 @@ async function executeCommand(ctx, deps, params) {
|
|
|
299
307
|
onSuccess: ({ error }) => {
|
|
300
308
|
if (error) {
|
|
301
309
|
foregroundSessionState.markIdle(session.id);
|
|
310
|
+
assistantRunState.clearRun(session.id, "session_command_api_error");
|
|
302
311
|
logger.error("[Commands] OpenCode API returned an error for session.command", {
|
|
303
312
|
sessionId: session.id,
|
|
304
313
|
command: params.commandName,
|
|
@@ -312,6 +321,7 @@ async function executeCommand(ctx, deps, params) {
|
|
|
312
321
|
},
|
|
313
322
|
onError: (error) => {
|
|
314
323
|
foregroundSessionState.markIdle(session.id);
|
|
324
|
+
assistantRunState.clearRun(session.id, "session_command_background_error");
|
|
315
325
|
logger.error("[Commands] session.command background task failed", {
|
|
316
326
|
sessionId: session.id,
|
|
317
327
|
command: params.commandName,
|
|
@@ -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" },
|
|
@@ -15,6 +16,7 @@ const COMMAND_DEFINITIONS = [
|
|
|
15
16
|
{ command: "commands", descriptionKey: "cmd.description.commands" },
|
|
16
17
|
{ command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
|
|
17
18
|
{ command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
|
|
19
|
+
{ command: "open", descriptionKey: "cmd.description.open" },
|
|
18
20
|
{ command: "help", descriptionKey: "cmd.description.help" },
|
|
19
21
|
];
|
|
20
22
|
export function getLocalizedBotCommands() {
|
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,
|