@grinev/opencode-telegram-bot 0.19.2 → 0.20.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 +5 -0
- package/README.md +5 -0
- package/dist/app/start-bot-app.js +4 -5
- package/dist/attach/service.js +34 -8
- package/dist/background-session/tracker.js +137 -0
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/detach.js +54 -0
- package/dist/bot/commands/ls.js +445 -0
- package/dist/bot/commands/open.js +9 -4
- package/dist/bot/commands/opencode-start.js +3 -3
- package/dist/bot/commands/projects.js +6 -2
- package/dist/bot/commands/sessions.js +233 -82
- package/dist/bot/commands/worktree.js +7 -2
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/index.js +86 -16
- package/dist/bot/utils/external-user-input.js +10 -2
- package/dist/bot/utils/send-downloaded-file.js +57 -0
- package/dist/bot/utils/switch-project.js +9 -14
- package/dist/config.js +1 -0
- package/dist/i18n/de.js +26 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/es.js +26 -0
- package/dist/i18n/fr.js +26 -0
- package/dist/i18n/ru.js +26 -0
- package/dist/i18n/zh.js +26 -0
- package/dist/interaction/busy.js +1 -1
- package/dist/interaction/manager.js +1 -1
- package/dist/model/context-limit.js +13 -2
- package/dist/opencode/auto-restart.js +4 -3
- package/dist/opencode/events.js +13 -2
- package/dist/opencode/ready-lifecycle.js +45 -0
- package/dist/opencode/ready-refresh.js +19 -1
- package/dist/pinned/manager.js +43 -7
- package/dist/scheduled-task/executor.js +3 -0
- package/dist/scheduled-task/runtime.js +2 -0
- package/dist/scheduled-task/schedule-parser.js +6 -0
- package/dist/scheduled-task/session-ignore.js +53 -0
- package/dist/settings/manager.js +12 -0
- package/dist/utils/opencode-error.js +13 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -83,6 +83,11 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
83
83
|
# Hide tool file edit documents sent as .txt attachments (default: false)
|
|
84
84
|
# HIDE_TOOL_FILE_MESSAGES=false
|
|
85
85
|
|
|
86
|
+
# Track background sessions in the currently selected project/worktree (default: true)
|
|
87
|
+
# Sends short notifications when detached or non-current sessions in the current directory reply,
|
|
88
|
+
# ask questions, or request permissions. Other projects/worktrees are not tracked.
|
|
89
|
+
# TRACK_BACKGROUND_SESSIONS=true
|
|
90
|
+
|
|
86
91
|
# Assistant message formatting mode (default: markdown)
|
|
87
92
|
# markdown = convert assistant replies to Telegram MarkdownV2
|
|
88
93
|
# raw = show assistant replies as plain text
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
28
28
|
- **Remote coding** — send prompts to OpenCode from anywhere, receive complete results with code sent as files
|
|
29
29
|
- **Session management** — create new sessions or continue existing ones, just like in the TUI
|
|
30
30
|
- **Track live session** — follow a live OpenCode CLI session; see [Track Existing Session](#track-existing-session)
|
|
31
|
+
- **Background session notifications** — get short notifications when detached or non-current sessions in the current project/worktree reply, ask questions, or request permissions
|
|
31
32
|
- **Live status** — pinned message with current project/worktree, model, context usage, and changed files list, updated in real time
|
|
32
33
|
- **Model switching** — pick models from OpenCode favorites and recent history directly in the chat (favorites are shown first)
|
|
33
34
|
- **Agent modes** — switch between Plan and Build modes on the fly
|
|
@@ -43,6 +44,7 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
43
44
|
- **Git worktree switching** — browse and switch between existing git worktrees for the current repository with `/worktree`
|
|
44
45
|
- **Security** — strict user ID whitelist; no one else can access your bot, even if they find it
|
|
45
46
|
- **Localization** — UI localization is supported for multiple languages (`BOT_LOCALE`)
|
|
47
|
+
- **Interactive file browser** — use `/ls` to browse files and directories inside the current project, open subdirectories, go back, and download files by tapping them
|
|
46
48
|
|
|
47
49
|
Planned features currently in development are listed in [Current Task List](PRODUCT.md#current-task-list).
|
|
48
50
|
|
|
@@ -130,10 +132,12 @@ opencode-telegram config
|
|
|
130
132
|
| `/status` | Server health, current project, session, and model info |
|
|
131
133
|
| `/new` | Create a new session |
|
|
132
134
|
| `/abort` | Abort the current task |
|
|
135
|
+
| `/detach` | Detach from the current session without stopping it |
|
|
133
136
|
| `/sessions` | Browse and switch between recent sessions |
|
|
134
137
|
| `/projects` | Switch between OpenCode projects |
|
|
135
138
|
| `/worktree` | Switch between existing git worktrees |
|
|
136
139
|
| `/open` | Add a project by browsing directories |
|
|
140
|
+
| `/ls` | List directory contents, then tap to open or download |
|
|
137
141
|
| `/tts` | Toggle audio replies |
|
|
138
142
|
| `/rename` | Rename the current session |
|
|
139
143
|
| `/commands` | Browse and run custom commands |
|
|
@@ -213,6 +217,7 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
213
217
|
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
214
218
|
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
215
219
|
| `HIDE_TOOL_FILE_MESSAGES` | Hide file edit documents sent as `.txt` attachments (`edit_*.txt`, `write_*.txt`) | No | `false` |
|
|
220
|
+
| `TRACK_BACKGROUND_SESSIONS` | Track detached/non-current sessions in the current selected project/worktree and send short notifications | No | `true` |
|
|
216
221
|
| `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
|
|
217
222
|
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
218
223
|
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { cleanupBotRuntime, createBot } from "../bot/index.js";
|
|
4
4
|
import { config } from "../config.js";
|
|
5
5
|
import { opencodeAutoRestartService } from "../opencode/auto-restart.js";
|
|
6
|
-
import {
|
|
6
|
+
import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../opencode/ready-refresh.js";
|
|
7
7
|
import { loadSettings } from "../settings/manager.js";
|
|
8
8
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
9
9
|
import { reconcileStoredModelSelection } from "../model/manager.js";
|
|
@@ -40,12 +40,11 @@ export async function startBotApp() {
|
|
|
40
40
|
logger.debug(`[Runtime] Application start mode: ${mode}`);
|
|
41
41
|
await loadSettings();
|
|
42
42
|
await reconcileStoredModelSelection();
|
|
43
|
-
|
|
44
|
-
if (!autoRestartHandledStartup) {
|
|
45
|
-
await refreshSessionCacheIfOpencodeReady("startup");
|
|
46
|
-
}
|
|
43
|
+
registerOpenCodeReadyRefreshHandler();
|
|
47
44
|
const bot = createBot();
|
|
48
45
|
await scheduledTaskRuntime.initialize(bot);
|
|
46
|
+
await opencodeAutoRestartService.start();
|
|
47
|
+
await notifyOpencodeReadyIfHealthy("startup");
|
|
49
48
|
let shutdownStarted = false;
|
|
50
49
|
let serviceStateCleared = false;
|
|
51
50
|
let shutdownTimeout = null;
|
package/dist/attach/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { opencodeClient } from "../opencode/client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isOpencodeServerHealthy } from "../opencode/ready-refresh.js";
|
|
3
3
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
4
4
|
import { pinnedMessageManager } from "../pinned/manager.js";
|
|
5
5
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
@@ -11,6 +11,7 @@ import { getCurrentSession } from "../session/manager.js";
|
|
|
11
11
|
import { getCurrentProject } from "../settings/manager.js";
|
|
12
12
|
import { attachManager } from "./manager.js";
|
|
13
13
|
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
|
|
14
15
|
function getAttachBusyStatus(sessionId, statuses) {
|
|
15
16
|
if (!statuses || typeof statuses !== "object") {
|
|
16
17
|
return false;
|
|
@@ -18,13 +19,16 @@ function getAttachBusyStatus(sessionId, statuses) {
|
|
|
18
19
|
const sessionStatus = statuses[sessionId];
|
|
19
20
|
return sessionStatus?.type === "busy";
|
|
20
21
|
}
|
|
21
|
-
async function ensureAttachPinnedSession({ api, chatId, session, }) {
|
|
22
|
+
async function ensureAttachPinnedSession({ api, chatId, session, forceFullRestore = false, }) {
|
|
22
23
|
if (!pinnedMessageManager.isInitialized()) {
|
|
23
24
|
pinnedMessageManager.initialize(api, chatId);
|
|
24
25
|
}
|
|
25
26
|
keyboardManager.initialize(api, chatId);
|
|
26
27
|
const pinnedState = pinnedMessageManager.getState();
|
|
27
28
|
if (pinnedState.sessionId === session.id && pinnedState.messageId) {
|
|
29
|
+
if (forceFullRestore) {
|
|
30
|
+
await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
|
|
31
|
+
}
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
30
34
|
if (pinnedState.messageId && pinnedState.sessionId === null) {
|
|
@@ -51,7 +55,12 @@ async function restorePendingQuestion(bot, chatId, sessionId, directory) {
|
|
|
51
55
|
directory,
|
|
52
56
|
});
|
|
53
57
|
if (error || !data) {
|
|
54
|
-
|
|
58
|
+
if (isExpectedOpencodeUnavailableError(error)) {
|
|
59
|
+
logger.warn("[Attach] OpenCode server unavailable; skipping pending question restore");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
logger.warn("[Attach] Failed to load pending questions during attach:", error);
|
|
63
|
+
}
|
|
55
64
|
return false;
|
|
56
65
|
}
|
|
57
66
|
const pendingQuestion = data.find((request) => request.sessionID === sessionId);
|
|
@@ -67,7 +76,12 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
|
|
|
67
76
|
directory,
|
|
68
77
|
});
|
|
69
78
|
if (error || !data) {
|
|
70
|
-
|
|
79
|
+
if (isExpectedOpencodeUnavailableError(error)) {
|
|
80
|
+
logger.warn("[Attach] OpenCode server unavailable; skipping pending permission restore");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
logger.warn("[Attach] Failed to load pending permissions during attach:", error);
|
|
84
|
+
}
|
|
71
85
|
return 0;
|
|
72
86
|
}
|
|
73
87
|
const pendingPermissions = data.filter((request) => request.sessionID === sessionId);
|
|
@@ -77,12 +91,13 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
|
|
|
77
91
|
return pendingPermissions.length;
|
|
78
92
|
}
|
|
79
93
|
export async function attachToSession(deps) {
|
|
80
|
-
const { bot, chatId, session, ensureEventSubscription } = deps;
|
|
94
|
+
const { bot, chatId, session, ensureEventSubscription, forceFullRestore = false } = deps;
|
|
81
95
|
const alreadyAttached = attachManager.isAttachedSession(session.id, session.directory);
|
|
82
96
|
await ensureAttachPinnedSession({
|
|
83
97
|
api: bot.api,
|
|
84
98
|
chatId,
|
|
85
99
|
session,
|
|
100
|
+
forceFullRestore,
|
|
86
101
|
});
|
|
87
102
|
if (!alreadyAttached) {
|
|
88
103
|
await ensureEventSubscription(session.directory);
|
|
@@ -98,7 +113,12 @@ export async function attachToSession(deps) {
|
|
|
98
113
|
directory: session.directory,
|
|
99
114
|
});
|
|
100
115
|
if (statusesError) {
|
|
101
|
-
|
|
116
|
+
if (isExpectedOpencodeUnavailableError(statusesError)) {
|
|
117
|
+
logger.warn("[Attach] OpenCode server unavailable; skipping session status restore");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.warn("[Attach] Failed to load session status during attach:", statusesError);
|
|
121
|
+
}
|
|
102
122
|
}
|
|
103
123
|
const busy = getAttachBusyStatus(session.id, statuses);
|
|
104
124
|
if (busy) {
|
|
@@ -110,7 +130,9 @@ export async function attachToSession(deps) {
|
|
|
110
130
|
await syncPinnedAttachState();
|
|
111
131
|
let restoredQuestion = false;
|
|
112
132
|
let restoredPermissions = 0;
|
|
113
|
-
if (!alreadyAttached
|
|
133
|
+
if ((!alreadyAttached || forceFullRestore) &&
|
|
134
|
+
!questionManager.isActive() &&
|
|
135
|
+
!permissionManager.isActive()) {
|
|
114
136
|
restoredQuestion = await restorePendingQuestion(bot, chatId, session.id, session.directory);
|
|
115
137
|
if (!restoredQuestion) {
|
|
116
138
|
restoredPermissions = await restorePendingPermissions(bot, chatId, session.id, session.directory);
|
|
@@ -134,11 +156,16 @@ export async function restoreAttachedCurrentSession(deps) {
|
|
|
134
156
|
return false;
|
|
135
157
|
}
|
|
136
158
|
try {
|
|
159
|
+
if (!(await isOpencodeServerHealthy())) {
|
|
160
|
+
logger.warn(`[Attach] OpenCode server is unavailable; skipping followed session restore: session=${currentSession.id}, directory=${currentSession.directory}`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
137
163
|
await attachToSession({
|
|
138
164
|
bot: deps.bot,
|
|
139
165
|
chatId: deps.chatId,
|
|
140
166
|
session: currentSession,
|
|
141
167
|
ensureEventSubscription: deps.ensureEventSubscription,
|
|
168
|
+
forceFullRestore: deps.forceFullRestore,
|
|
142
169
|
});
|
|
143
170
|
logger.info(`[Attach] Restored followed session on startup: session=${currentSession.id}, directory=${currentSession.directory}`);
|
|
144
171
|
return true;
|
|
@@ -152,7 +179,6 @@ export function detachAttachedSession(reason) {
|
|
|
152
179
|
if (!attachManager.isAttached()) {
|
|
153
180
|
return;
|
|
154
181
|
}
|
|
155
|
-
stopEventListening();
|
|
156
182
|
summaryAggregator.clear();
|
|
157
183
|
attachManager.clear(reason);
|
|
158
184
|
void syncPinnedAttachState();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { isScheduledTaskSessionIgnored } from "../scheduled-task/session-ignore.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
class BackgroundSessionTracker {
|
|
4
|
+
directory = null;
|
|
5
|
+
onNotification = null;
|
|
6
|
+
sessionTitles = new Map();
|
|
7
|
+
childSessionIds = new Set();
|
|
8
|
+
completedAssistantMessageIds = new Set();
|
|
9
|
+
pendingAssistantResponsesBySessionId = new Map();
|
|
10
|
+
questionRequestIds = new Set();
|
|
11
|
+
permissionRequestIds = new Set();
|
|
12
|
+
setDirectory(directory) {
|
|
13
|
+
if (this.directory === directory) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.clear();
|
|
17
|
+
this.directory = directory;
|
|
18
|
+
}
|
|
19
|
+
setOnNotification(callback) {
|
|
20
|
+
this.onNotification = callback;
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.directory = null;
|
|
24
|
+
this.sessionTitles.clear();
|
|
25
|
+
this.childSessionIds.clear();
|
|
26
|
+
this.completedAssistantMessageIds.clear();
|
|
27
|
+
this.pendingAssistantResponsesBySessionId.clear();
|
|
28
|
+
this.questionRequestIds.clear();
|
|
29
|
+
this.permissionRequestIds.clear();
|
|
30
|
+
}
|
|
31
|
+
processEvent(event, currentSessionId) {
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case "session.created":
|
|
34
|
+
case "session.updated":
|
|
35
|
+
this.handleSessionInfo(event.properties);
|
|
36
|
+
break;
|
|
37
|
+
case "message.updated":
|
|
38
|
+
this.handleMessageUpdated(event.properties, currentSessionId);
|
|
39
|
+
break;
|
|
40
|
+
case "session.idle":
|
|
41
|
+
this.handleSessionIdle(event.properties, currentSessionId);
|
|
42
|
+
break;
|
|
43
|
+
case "question.asked":
|
|
44
|
+
this.handleRequestEvent("question_asked", event.properties, currentSessionId);
|
|
45
|
+
break;
|
|
46
|
+
case "permission.asked":
|
|
47
|
+
this.handleRequestEvent("permission_asked", event.properties, currentSessionId);
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
handleSessionInfo(properties) {
|
|
54
|
+
const info = properties.info;
|
|
55
|
+
if (!info?.id) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const title = info.title?.trim();
|
|
59
|
+
if (title) {
|
|
60
|
+
this.sessionTitles.set(info.id, title);
|
|
61
|
+
}
|
|
62
|
+
if (info.parentID) {
|
|
63
|
+
this.childSessionIds.add(info.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
handleMessageUpdated(properties, currentSessionId) {
|
|
67
|
+
const info = properties.info;
|
|
68
|
+
const sessionId = info?.sessionID;
|
|
69
|
+
const messageId = info?.id;
|
|
70
|
+
if (!sessionId || !messageId || info?.role !== "assistant" || !info.time?.completed) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (this.shouldIgnoreSession(sessionId, currentSessionId)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (this.completedAssistantMessageIds.has(messageId)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.completedAssistantMessageIds.add(messageId);
|
|
80
|
+
this.pendingAssistantResponsesBySessionId.set(sessionId, { messageId });
|
|
81
|
+
}
|
|
82
|
+
handleSessionIdle(properties, currentSessionId) {
|
|
83
|
+
const sessionId = properties.sessionID;
|
|
84
|
+
if (!sessionId || this.shouldIgnoreSession(sessionId, currentSessionId)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const pendingResponse = this.pendingAssistantResponsesBySessionId.get(sessionId);
|
|
88
|
+
if (!pendingResponse) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.pendingAssistantResponsesBySessionId.delete(sessionId);
|
|
92
|
+
this.emitNotification({
|
|
93
|
+
kind: "assistant_response",
|
|
94
|
+
sessionId,
|
|
95
|
+
sessionTitle: this.sessionTitles.get(sessionId),
|
|
96
|
+
messageId: pendingResponse.messageId,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
handleRequestEvent(kind, properties, currentSessionId) {
|
|
100
|
+
const { id, sessionID: sessionId } = properties;
|
|
101
|
+
if (!id || !sessionId) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.shouldIgnoreSession(sessionId, currentSessionId)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const deliveredRequestIds = kind === "question_asked" ? this.questionRequestIds : this.permissionRequestIds;
|
|
108
|
+
if (deliveredRequestIds.has(id)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
deliveredRequestIds.add(id);
|
|
112
|
+
this.emitNotification({
|
|
113
|
+
kind,
|
|
114
|
+
sessionId,
|
|
115
|
+
sessionTitle: this.sessionTitles.get(sessionId),
|
|
116
|
+
requestId: id,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
shouldIgnoreSession(sessionId, currentSessionId) {
|
|
120
|
+
return (sessionId === currentSessionId ||
|
|
121
|
+
this.childSessionIds.has(sessionId) ||
|
|
122
|
+
isScheduledTaskSessionIgnored(sessionId));
|
|
123
|
+
}
|
|
124
|
+
emitNotification(notification) {
|
|
125
|
+
if (!this.onNotification) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const callback = this.onNotification;
|
|
129
|
+
setImmediate(() => {
|
|
130
|
+
Promise.resolve(callback(notification)).catch((error) => {
|
|
131
|
+
logger.error("[BackgroundSessionTracker] Failed to deliver notification:", error);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export { BackgroundSessionTracker };
|
|
137
|
+
export const backgroundSessionTracker = new BackgroundSessionTracker();
|
|
@@ -7,6 +7,7 @@ const COMMAND_DEFINITIONS = [
|
|
|
7
7
|
{ command: "status", descriptionKey: "cmd.description.status" },
|
|
8
8
|
{ command: "new", descriptionKey: "cmd.description.new" },
|
|
9
9
|
{ command: "abort", descriptionKey: "cmd.description.stop" },
|
|
10
|
+
{ command: "detach", descriptionKey: "cmd.description.detach" },
|
|
10
11
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
12
|
{ command: "tts", descriptionKey: "cmd.description.tts" },
|
|
12
13
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
@@ -20,6 +21,7 @@ const COMMAND_DEFINITIONS = [
|
|
|
20
21
|
{ command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
|
|
21
22
|
{ command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
|
|
22
23
|
{ command: "open", descriptionKey: "cmd.description.open" },
|
|
24
|
+
{ command: "ls", descriptionKey: "cmd.description.ls" },
|
|
23
25
|
{ command: "help", descriptionKey: "cmd.description.help" },
|
|
24
26
|
];
|
|
25
27
|
export function getLocalizedBotCommands() {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
2
|
+
import { clearSession, getCurrentSession } from "../../session/manager.js";
|
|
3
|
+
import { detachAttachedSession } from "../../attach/service.js";
|
|
4
|
+
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
5
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
6
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
|
+
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
8
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
9
|
+
import { clearPromptResponseMode } from "../handlers/prompt.js";
|
|
10
|
+
import { logger } from "../../utils/logger.js";
|
|
11
|
+
import { t } from "../../i18n/index.js";
|
|
12
|
+
export async function detachCommand(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const currentProject = getCurrentProject();
|
|
15
|
+
if (!currentProject) {
|
|
16
|
+
await ctx.reply(t("detach.project_not_selected"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const currentSession = getCurrentSession();
|
|
20
|
+
if (!currentSession) {
|
|
21
|
+
await ctx.reply(t("detach.no_active_session"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
detachAttachedSession("detach_command");
|
|
25
|
+
clearPromptResponseMode(currentSession.id);
|
|
26
|
+
foregroundSessionState.markIdle(currentSession.id);
|
|
27
|
+
assistantRunState.clearRun(currentSession.id, "detach_command");
|
|
28
|
+
clearAllInteractionState("detach_command");
|
|
29
|
+
clearSession();
|
|
30
|
+
if (pinnedMessageManager.isInitialized()) {
|
|
31
|
+
try {
|
|
32
|
+
await pinnedMessageManager.clear();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
logger.error("[Detach] Failed to clear pinned message:", error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (ctx.chat) {
|
|
39
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
40
|
+
}
|
|
41
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
42
|
+
const contextLimit = pinnedMessageManager.getContextLimit();
|
|
43
|
+
keyboardManager.updateContext(0, contextLimit);
|
|
44
|
+
const keyboard = keyboardManager.getKeyboard();
|
|
45
|
+
logger.info(`[Detach] Detached from session: id=${currentSession.id}, title="${currentSession.title}", project=${currentProject.worktree}`);
|
|
46
|
+
await ctx.reply(t("detach.success", { title: currentSession.title }), {
|
|
47
|
+
...(keyboard ? { reply_markup: keyboard } : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.error("[Detach] Failed to detach from current session:", error);
|
|
52
|
+
await ctx.reply(t("detach.error"));
|
|
53
|
+
}
|
|
54
|
+
}
|