@grinev/opencode-telegram-bot 0.17.0 → 0.18.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/README.md +15 -1
- package/dist/attach/manager.js +66 -0
- package/dist/attach/service.js +166 -0
- package/dist/bot/commands/abort.js +0 -4
- package/dist/bot/commands/commands.js +13 -3
- package/dist/bot/commands/new.js +10 -18
- package/dist/bot/commands/sessions.js +19 -20
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/handlers/prompt.js +26 -31
- package/dist/bot/index.js +76 -8
- package/dist/bot/middleware/interaction-guard.js +1 -1
- package/dist/bot/utils/busy-guard.js +3 -2
- package/dist/bot/utils/external-user-input.js +46 -0
- package/dist/bot/utils/switch-project.js +2 -0
- package/dist/external-input/suppression.js +54 -0
- package/dist/i18n/de.js +15 -0
- package/dist/i18n/en.js +15 -0
- package/dist/i18n/es.js +15 -0
- package/dist/i18n/fr.js +15 -0
- package/dist/i18n/ru.js +15 -0
- package/dist/i18n/zh.js +15 -0
- package/dist/interaction/guard.js +2 -1
- package/dist/opencode/events.js +13 -1
- package/dist/pinned/manager.js +18 -0
- package/dist/summary/aggregator.js +54 -9
- package/package.json +1 -1
package/dist/bot/index.js
CHANGED
|
@@ -66,7 +66,11 @@ import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
|
66
66
|
import { assistantRunState } from "./assistant-run-state.js";
|
|
67
67
|
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
68
68
|
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
69
|
+
import { attachManager } from "../attach/manager.js";
|
|
70
|
+
import { markAttachedSessionBusy, markAttachedSessionIdle, restoreAttachedCurrentSession, } from "../attach/service.js";
|
|
71
|
+
import { externalUserInputSuppressionManager } from "../external-input/suppression.js";
|
|
69
72
|
import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "./utils/assistant-rendering.js";
|
|
73
|
+
import { deliverExternalUserInputNotification } from "./utils/external-user-input.js";
|
|
70
74
|
let botInstance = null;
|
|
71
75
|
let chatIdInstance = null;
|
|
72
76
|
let commandsInitialized = false;
|
|
@@ -263,6 +267,27 @@ function getToolStreamKey(tool) {
|
|
|
263
267
|
}
|
|
264
268
|
return "default";
|
|
265
269
|
}
|
|
270
|
+
function getEventSessionId(event) {
|
|
271
|
+
const properties = event.properties;
|
|
272
|
+
return properties.sessionID || properties.info?.sessionID || properties.part?.sessionID || null;
|
|
273
|
+
}
|
|
274
|
+
function shouldMarkAttachedBusyFromEvent(event) {
|
|
275
|
+
switch (event.type) {
|
|
276
|
+
case "session.status":
|
|
277
|
+
return event.properties.status?.type === "busy";
|
|
278
|
+
case "message.updated": {
|
|
279
|
+
const info = event.properties.info;
|
|
280
|
+
return info?.role === "assistant" && !info.time?.completed;
|
|
281
|
+
}
|
|
282
|
+
case "message.part.updated":
|
|
283
|
+
case "message.part.delta":
|
|
284
|
+
case "question.asked":
|
|
285
|
+
case "permission.asked":
|
|
286
|
+
return true;
|
|
287
|
+
default:
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
266
291
|
async function ensureCommandsInitialized(ctx, next) {
|
|
267
292
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
268
293
|
await next();
|
|
@@ -388,6 +413,26 @@ async function ensureEventSubscription(directory) {
|
|
|
388
413
|
}
|
|
389
414
|
});
|
|
390
415
|
});
|
|
416
|
+
summaryAggregator.setOnExternalUserInput(async (sessionId, _messageId, messageText) => {
|
|
417
|
+
void enqueueSessionCompletionTask(sessionId, async () => {
|
|
418
|
+
if (!botInstance || !chatIdInstance) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await deliverExternalUserInputNotification({
|
|
423
|
+
api: botInstance.api,
|
|
424
|
+
chatId: chatIdInstance,
|
|
425
|
+
currentSessionId: getCurrentSession()?.id ?? null,
|
|
426
|
+
sessionId,
|
|
427
|
+
text: messageText,
|
|
428
|
+
consumeSuppressedInput: (incomingSessionId, incomingText) => externalUserInputSuppressionManager.consume(incomingSessionId, incomingText),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
logger.error("[Bot] Failed to deliver external user input to Telegram:", err);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
391
436
|
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
392
437
|
if (!botInstance || !chatIdInstance) {
|
|
393
438
|
logger.error("Bot or chat ID not available for sending tool notification");
|
|
@@ -461,18 +506,19 @@ async function ensureEventSubscription(directory) {
|
|
|
461
506
|
logger.error("Failed to send file to Telegram:", err);
|
|
462
507
|
}
|
|
463
508
|
});
|
|
464
|
-
summaryAggregator.setOnQuestion(async (questions, requestID) => {
|
|
509
|
+
summaryAggregator.setOnQuestion(async (questions, requestID, sessionId) => {
|
|
465
510
|
if (!botInstance || !chatIdInstance) {
|
|
466
511
|
logger.error("Bot or chat ID not available for showing questions");
|
|
467
512
|
return;
|
|
468
513
|
}
|
|
469
514
|
const currentSession = getCurrentSession();
|
|
470
|
-
if (currentSession) {
|
|
471
|
-
|
|
472
|
-
toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
|
|
473
|
-
toolCallStreamer.flushSession(currentSession.id, "question_asked"),
|
|
474
|
-
]);
|
|
515
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
516
|
+
return;
|
|
475
517
|
}
|
|
518
|
+
await Promise.all([
|
|
519
|
+
toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
|
|
520
|
+
toolCallStreamer.flushSession(currentSession.id, "question_asked"),
|
|
521
|
+
]);
|
|
476
522
|
if (questionManager.isActive()) {
|
|
477
523
|
logger.warn("[Bot] Replacing active poll with a new one");
|
|
478
524
|
const previousMessageIds = questionManager.getMessageIds();
|
|
@@ -503,6 +549,10 @@ async function ensureEventSubscription(directory) {
|
|
|
503
549
|
logger.error("Bot or chat ID not available for showing permission request");
|
|
504
550
|
return;
|
|
505
551
|
}
|
|
552
|
+
const currentSession = getCurrentSession();
|
|
553
|
+
if (!currentSession || currentSession.id !== request.sessionID) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
506
556
|
await Promise.all([
|
|
507
557
|
toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
|
|
508
558
|
toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
|
|
@@ -583,6 +633,7 @@ async function ensureEventSubscription(directory) {
|
|
|
583
633
|
}
|
|
584
634
|
});
|
|
585
635
|
summaryAggregator.setOnSessionIdle(async (sessionId) => {
|
|
636
|
+
await markAttachedSessionIdle(sessionId);
|
|
586
637
|
await sessionCompletionTasks.get(sessionId)?.catch(() => undefined);
|
|
587
638
|
const completedRun = assistantRunState.finishRun(sessionId, "session_idle");
|
|
588
639
|
clearPromptResponseMode(sessionId);
|
|
@@ -627,6 +678,7 @@ async function ensureEventSubscription(directory) {
|
|
|
627
678
|
}
|
|
628
679
|
});
|
|
629
680
|
summaryAggregator.setOnSessionError(async (sessionId, message) => {
|
|
681
|
+
await markAttachedSessionIdle(sessionId);
|
|
630
682
|
if (!botInstance || !chatIdInstance) {
|
|
631
683
|
clearPromptResponseMode(sessionId);
|
|
632
684
|
assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
|
|
@@ -706,6 +758,11 @@ async function ensureEventSubscription(directory) {
|
|
|
706
758
|
});
|
|
707
759
|
logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
|
|
708
760
|
subscribeToEvents(directory, (event) => {
|
|
761
|
+
const attached = attachManager.getSnapshot();
|
|
762
|
+
const eventSessionId = getEventSessionId(event);
|
|
763
|
+
if (attached && eventSessionId === attached.sessionId && shouldMarkAttachedBusyFromEvent(event)) {
|
|
764
|
+
void markAttachedSessionBusy(attached.sessionId);
|
|
765
|
+
}
|
|
709
766
|
if (event.type === "session.created" || event.type === "session.updated") {
|
|
710
767
|
const info = event.properties.info;
|
|
711
768
|
if (info?.directory) {
|
|
@@ -723,6 +780,7 @@ async function ensureEventSubscription(directory) {
|
|
|
723
780
|
export function createBot() {
|
|
724
781
|
clearAllInteractionState("bot_startup");
|
|
725
782
|
sessionCompletionTasks.clear();
|
|
783
|
+
attachManager.clear("bot_startup");
|
|
726
784
|
assistantRunState.clearAll("bot_startup");
|
|
727
785
|
if (heartbeatTimer) {
|
|
728
786
|
clearInterval(heartbeatTimer);
|
|
@@ -748,6 +806,8 @@ export function createBot() {
|
|
|
748
806
|
};
|
|
749
807
|
}
|
|
750
808
|
const bot = new Bot(config.telegram.token, botOptions);
|
|
809
|
+
botInstance = bot;
|
|
810
|
+
chatIdInstance = config.telegram.allowedUserId;
|
|
751
811
|
// Heartbeat for diagnostics: verify the event loop is not blocked
|
|
752
812
|
let heartbeatCounter = 0;
|
|
753
813
|
heartbeatTimer = setInterval(() => {
|
|
@@ -806,7 +866,7 @@ export function createBot() {
|
|
|
806
866
|
bot.command("worktree", worktreeCommand);
|
|
807
867
|
bot.command("open", openCommand);
|
|
808
868
|
bot.command("sessions", sessionsCommand);
|
|
809
|
-
bot.command("new", newCommand);
|
|
869
|
+
bot.command("new", (ctx) => newCommand(ctx, { bot, ensureEventSubscription }));
|
|
810
870
|
bot.command("abort", abortCommand);
|
|
811
871
|
bot.command("task", taskCommand);
|
|
812
872
|
bot.command("tasklist", taskListCommand);
|
|
@@ -827,7 +887,7 @@ export function createBot() {
|
|
|
827
887
|
// Clean up path index when the open-directory menu is cancelled
|
|
828
888
|
clearOpenPathIndex();
|
|
829
889
|
}
|
|
830
|
-
const handledSession = await handleSessionSelect(ctx);
|
|
890
|
+
const handledSession = await handleSessionSelect(ctx, { bot, ensureEventSubscription });
|
|
831
891
|
const handledProject = await handleProjectSelect(ctx);
|
|
832
892
|
const handledWorktree = await handleWorktreeCallback(ctx);
|
|
833
893
|
const handledOpen = await handleOpenCallback(ctx);
|
|
@@ -960,6 +1020,14 @@ export function createBot() {
|
|
|
960
1020
|
});
|
|
961
1021
|
// Voice and audio message handlers (STT transcription -> prompt)
|
|
962
1022
|
const voicePromptDeps = { bot, ensureEventSubscription };
|
|
1023
|
+
safeBackgroundTask({
|
|
1024
|
+
taskName: "bot.restoreFollowedSession",
|
|
1025
|
+
task: () => restoreAttachedCurrentSession({
|
|
1026
|
+
bot,
|
|
1027
|
+
chatId: config.telegram.allowedUserId,
|
|
1028
|
+
ensureEventSubscription,
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
963
1031
|
bot.on("message:voice", async (ctx) => {
|
|
964
1032
|
logger.debug(`[Bot] Received voice message, chatId=${ctx.chat.id}`);
|
|
965
1033
|
botInstance = bot;
|
|
@@ -80,7 +80,7 @@ export async function interactionGuardMiddleware(ctx, next) {
|
|
|
80
80
|
const message = decision.busy
|
|
81
81
|
? decision.state?.kind === "question" || decision.state?.kind === "permission"
|
|
82
82
|
? getInteractionBlockedMessage(decision.reason, decision.state.kind)
|
|
83
|
-
: t("
|
|
83
|
+
: t("bot.session_busy")
|
|
84
84
|
: getInteractionBlockedMessage(decision.reason, decision.state?.kind);
|
|
85
85
|
logger.debug(`[InteractionGuard] Blocked input: interactionKind=${decision.state?.kind || "none"}, inputType=${decision.inputType}, reason=${decision.reason || "unknown"}, command=${decision.command || "-"}, busy=${decision.busy ? "yes" : "no"}`);
|
|
86
86
|
if (ctx.callbackQuery) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
2
|
+
import { attachManager } from "../../attach/manager.js";
|
|
2
3
|
import { t } from "../../i18n/index.js";
|
|
3
4
|
export function isForegroundBusy() {
|
|
4
|
-
return foregroundSessionState.isBusy();
|
|
5
|
+
return foregroundSessionState.isBusy() || attachManager.isBusy();
|
|
5
6
|
}
|
|
6
7
|
export async function replyBusyBlocked(ctx) {
|
|
7
|
-
const message = t("
|
|
8
|
+
const message = t("bot.session_busy");
|
|
8
9
|
if (ctx.callbackQuery) {
|
|
9
10
|
await ctx.answerCallbackQuery({ text: message }).catch(() => { });
|
|
10
11
|
return;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { t } from "../../i18n/index.js";
|
|
2
|
+
import { escapePlainTextForTelegramMarkdownV2 } from "../../summary/formatter.js";
|
|
3
|
+
import { sendBotText } from "./telegram-text.js";
|
|
4
|
+
function normalizeExternalUserInputText(text) {
|
|
5
|
+
return text.replace(/\r\n/g, "\n").trim();
|
|
6
|
+
}
|
|
7
|
+
function buildQuotedPlainText(text) {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((line) => (line.length > 0 ? `> ${line}` : ">"))
|
|
11
|
+
.join("\n");
|
|
12
|
+
}
|
|
13
|
+
function buildQuotedMarkdownText(text) {
|
|
14
|
+
return text
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((line) => line.length > 0 ? `> ${escapePlainTextForTelegramMarkdownV2(line)}` : ">")
|
|
17
|
+
.join("\n");
|
|
18
|
+
}
|
|
19
|
+
export function buildExternalUserInputNotification(text) {
|
|
20
|
+
const normalizedText = normalizeExternalUserInputText(text);
|
|
21
|
+
if (!normalizedText) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const title = `👤 ${t("bot.external_user_input")}`;
|
|
25
|
+
return {
|
|
26
|
+
text: `${escapePlainTextForTelegramMarkdownV2(title)}\n\n${buildQuotedMarkdownText(normalizedText)}`,
|
|
27
|
+
rawFallbackText: `${title}\n\n${buildQuotedPlainText(normalizedText)}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function deliverExternalUserInputNotification({ api, chatId, currentSessionId, sessionId, text, consumeSuppressedInput, }) {
|
|
31
|
+
const notification = buildExternalUserInputNotification(text);
|
|
32
|
+
if (!notification || currentSessionId !== sessionId) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (consumeSuppressedInput(sessionId, text)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
await sendBotText({
|
|
39
|
+
api,
|
|
40
|
+
chatId,
|
|
41
|
+
text: notification.text,
|
|
42
|
+
rawFallbackText: notification.rawFallbackText,
|
|
43
|
+
format: "markdown_v2",
|
|
44
|
+
});
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
@@ -3,6 +3,7 @@ import { clearSession } from "../../session/manager.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";
|
|
6
|
+
import { detachAttachedSession } from "../../attach/service.js";
|
|
6
7
|
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
7
8
|
import { getStoredModel } from "../../model/manager.js";
|
|
8
9
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
@@ -23,6 +24,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
23
24
|
* @param reason short tag for `clearAllInteractionState` (e.g. "project_switched")
|
|
24
25
|
*/
|
|
25
26
|
export async function switchToProject(ctx, project, reason) {
|
|
27
|
+
detachAttachedSession(reason);
|
|
26
28
|
setCurrentProject(project);
|
|
27
29
|
clearSession();
|
|
28
30
|
summaryAggregator.clear();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const SUPPRESSION_TTL_MS = 60_000;
|
|
2
|
+
function normalizeExternalUserInputText(text) {
|
|
3
|
+
return text.replace(/\r\n/g, "\n").trim();
|
|
4
|
+
}
|
|
5
|
+
class ExternalUserInputSuppressionManager {
|
|
6
|
+
entriesBySession = new Map();
|
|
7
|
+
register(sessionId, text, now = Date.now()) {
|
|
8
|
+
const normalizedText = normalizeExternalUserInputText(text);
|
|
9
|
+
if (!sessionId || !normalizedText) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
this.prune(now);
|
|
13
|
+
const sessionEntries = this.entriesBySession.get(sessionId) ?? [];
|
|
14
|
+
sessionEntries.push({ text: normalizedText, createdAt: now });
|
|
15
|
+
this.entriesBySession.set(sessionId, sessionEntries);
|
|
16
|
+
}
|
|
17
|
+
consume(sessionId, text, now = Date.now()) {
|
|
18
|
+
const normalizedText = normalizeExternalUserInputText(text);
|
|
19
|
+
if (!sessionId || !normalizedText) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
this.prune(now);
|
|
23
|
+
const sessionEntries = this.entriesBySession.get(sessionId);
|
|
24
|
+
if (!sessionEntries?.length) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const entryIndex = sessionEntries.findIndex((entry) => entry.text === normalizedText);
|
|
28
|
+
if (entryIndex < 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
sessionEntries.splice(entryIndex, 1);
|
|
32
|
+
if (sessionEntries.length === 0) {
|
|
33
|
+
this.entriesBySession.delete(sessionId);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
clearAll() {
|
|
38
|
+
this.entriesBySession.clear();
|
|
39
|
+
}
|
|
40
|
+
__resetForTests() {
|
|
41
|
+
this.clearAll();
|
|
42
|
+
}
|
|
43
|
+
prune(now) {
|
|
44
|
+
for (const [sessionId, sessionEntries] of this.entriesBySession.entries()) {
|
|
45
|
+
const activeEntries = sessionEntries.filter((entry) => now - entry.createdAt <= SUPPRESSION_TTL_MS);
|
|
46
|
+
if (activeEntries.length === 0) {
|
|
47
|
+
this.entriesBySession.delete(sessionId);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
this.entriesBySession.set(sessionId, activeEntries);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export const externalUserInputSuppressionManager = new ExternalUserInputSuppressionManager();
|
package/dist/i18n/de.js
CHANGED
|
@@ -48,6 +48,7 @@ export const de = {
|
|
|
48
48
|
"bot.prompt_send_error": "Anfrage konnte nicht an OpenCode gesendet werden.",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode meldete einen Fehler: {message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /abort abbrechen.",
|
|
51
|
+
"bot.external_user_input": "Externe Benutzereingabe",
|
|
51
52
|
"bot.unknown_command": "⚠️ Unbekannter Befehl: {command}. Nutze /help, um verfügbare Befehle zu sehen.",
|
|
52
53
|
"bot.photo_downloading": "⏳ Lade Foto herunter...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ Foto ist zu groß (max. {maxSizeMb}MB)",
|
|
@@ -113,6 +114,17 @@ export const de = {
|
|
|
113
114
|
"sessions.preview.title": "Letzte Nachrichten:",
|
|
114
115
|
"sessions.preview.you": "Du:",
|
|
115
116
|
"sessions.preview.agent": "Agent:",
|
|
117
|
+
"attach.project_not_selected": "🏗 Projekt ist nicht ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Keine Sitzung ausgewählt.\n\nWähle zuerst mit /sessions eine Sitzung aus.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ Die ausgewählte Sitzung passt nicht zum aktuellen Projekt. Wähle sie über /sessions erneut aus.",
|
|
120
|
+
"attach.connected": "✅ Mit Sitzung verbunden: {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Bereits mit Sitzung verbunden: {title}",
|
|
122
|
+
"attach.status.idle_message": "Status: idle. Warte auf neue Ereignisse.",
|
|
123
|
+
"attach.status.busy_message": "Status: busy. Neue Prompts sind vorübergehend blockiert.",
|
|
124
|
+
"attach.restored_question": "Eine ausstehende Frage für diese Sitzung wurde wiederhergestellt.",
|
|
125
|
+
"attach.restored_permissions": "Ausstehende Berechtigungsanfragen wiederhergestellt: {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Zum Trennen einfach zu einer anderen Sitzung oder einem anderen Projekt wechseln.",
|
|
127
|
+
"attach.error": "🔴 Verbindung mit der aktuellen Sitzung fehlgeschlagen.",
|
|
116
128
|
"new.project_not_selected": "🏗 Projekt ist nicht ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.",
|
|
117
129
|
"new.created": "✅ Neue Sitzung erstellt: {title}",
|
|
118
130
|
"new.create_error": "🔴 OpenCode-Server ist nicht verfügbar oder beim Erstellen der Sitzung ist ein Fehler aufgetreten.",
|
|
@@ -232,6 +244,9 @@ export const de = {
|
|
|
232
244
|
"pinned.line.project": "Projekt: {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree: {worktree}",
|
|
234
246
|
"pinned.line.model": "Modell: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "aktiv, idle",
|
|
249
|
+
"pinned.attach.status.busy": "aktiv, busy",
|
|
235
250
|
"pinned.line.context": "Kontext: {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "Kosten: {cost} ausgegeben",
|
|
237
252
|
"subagent.header": "Subagent {agent}: {description}",
|
package/dist/i18n/en.js
CHANGED
|
@@ -48,6 +48,7 @@ export const en = {
|
|
|
48
48
|
"bot.prompt_send_error": "Failed to send request to OpenCode.",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode returned an error: {message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /abort to abort.",
|
|
51
|
+
"bot.external_user_input": "External user input",
|
|
51
52
|
"bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
|
|
52
53
|
"bot.photo_downloading": "⏳ Downloading photo...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ Photo is too large (max {maxSizeMb}MB)",
|
|
@@ -113,6 +114,17 @@ export const en = {
|
|
|
113
114
|
"sessions.preview.title": "Recent messages:",
|
|
114
115
|
"sessions.preview.you": "You:",
|
|
115
116
|
"sessions.preview.agent": "Agent:",
|
|
117
|
+
"attach.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Session is not selected.\n\nFirst choose a session with /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ The selected session does not match the current project. Choose the session again via /sessions.",
|
|
120
|
+
"attach.connected": "✅ Connected to session: {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Already connected to session: {title}",
|
|
122
|
+
"attach.status.idle_message": "Status: idle. Waiting for new events.",
|
|
123
|
+
"attach.status.busy_message": "Status: busy. New prompts are temporarily blocked.",
|
|
124
|
+
"attach.restored_question": "Recovered a pending question for this session.",
|
|
125
|
+
"attach.restored_permissions": "Recovered pending permission requests: {count}.",
|
|
126
|
+
"attach.disconnect_hint": "To disconnect, switch to another session or project.",
|
|
127
|
+
"attach.error": "🔴 Failed to attach to the current session.",
|
|
116
128
|
"new.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
|
|
117
129
|
"new.created": "✅ New session created: {title}",
|
|
118
130
|
"new.create_error": "🔴 OpenCode Server is unavailable or an error occurred while creating session.",
|
|
@@ -232,6 +244,9 @@ export const en = {
|
|
|
232
244
|
"pinned.line.project": "Project: {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree: {worktree}",
|
|
234
246
|
"pinned.line.model": "Model: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "active, idle",
|
|
249
|
+
"pinned.attach.status.busy": "active, busy",
|
|
235
250
|
"pinned.line.context": "Context: {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "Cost: {cost} spent",
|
|
237
252
|
"subagent.header": "Subagent {agent}: {description}",
|
package/dist/i18n/es.js
CHANGED
|
@@ -48,6 +48,7 @@ export const es = {
|
|
|
48
48
|
"bot.prompt_send_error": "No se pudo enviar la solicitud a OpenCode.",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode devolvió un error: {message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /abort para detenerlo.",
|
|
51
|
+
"bot.external_user_input": "Entrada externa del usuario",
|
|
51
52
|
"bot.unknown_command": "⚠️ Comando desconocido: {command}. Usa /help para ver los comandos disponibles.",
|
|
52
53
|
"bot.photo_downloading": "⏳ Descargando foto...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ La foto es demasiado grande (max {maxSizeMb}MB)",
|
|
@@ -113,6 +114,17 @@ export const es = {
|
|
|
113
114
|
"sessions.preview.title": "Mensajes recientes:",
|
|
114
115
|
"sessions.preview.you": "Tú:",
|
|
115
116
|
"sessions.preview.agent": "Agente:",
|
|
117
|
+
"attach.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 No hay una sesión seleccionada.\n\nPrimero selecciona una sesión con /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ La sesión seleccionada no coincide con el proyecto actual. Vuelve a seleccionarla con /sessions.",
|
|
120
|
+
"attach.connected": "✅ Conectado a la sesión: {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Ya estás conectado a la sesión: {title}",
|
|
122
|
+
"attach.status.idle_message": "Estado: idle. Esperando nuevos eventos.",
|
|
123
|
+
"attach.status.busy_message": "Estado: busy. Los nuevos prompts están bloqueados temporalmente.",
|
|
124
|
+
"attach.restored_question": "Se restauró una pregunta pendiente para esta sesión.",
|
|
125
|
+
"attach.restored_permissions": "Se restauraron solicitudes de permiso pendientes: {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Para desconectarte, cambia a otra sesión o proyecto.",
|
|
127
|
+
"attach.error": "🔴 No se pudo conectar a la sesión actual.",
|
|
116
128
|
"new.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
|
|
117
129
|
"new.created": "✅ Sesión nueva creada: {title}",
|
|
118
130
|
"new.create_error": "🔴 OpenCode Server no está disponible u ocurrió un error al crear la sesión.",
|
|
@@ -232,6 +244,9 @@ export const es = {
|
|
|
232
244
|
"pinned.line.project": "Proyecto: {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree: {worktree}",
|
|
234
246
|
"pinned.line.model": "Modelo: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "activo, idle",
|
|
249
|
+
"pinned.attach.status.busy": "activo, busy",
|
|
235
250
|
"pinned.line.context": "Contexto: {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "Costo: {cost} gastado",
|
|
237
252
|
"subagent.header": "Subagente {agent}: {description}",
|
package/dist/i18n/fr.js
CHANGED
|
@@ -48,6 +48,7 @@ export const fr = {
|
|
|
48
48
|
"bot.prompt_send_error": "Impossible d'envoyer la requête à OpenCode.",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode a renvoyé une erreur : {message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\nLe fournisseur renvoie la même erreur à chaque nouvelle tentative. Utilisez /abort pour arrêter.",
|
|
51
|
+
"bot.external_user_input": "Entrée utilisateur externe",
|
|
51
52
|
"bot.unknown_command": "⚠️ Commande inconnue : {command}. Utilisez /help pour voir les commandes disponibles.",
|
|
52
53
|
"bot.photo_downloading": "⏳ Téléchargement de la photo...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ La photo est trop volumineuse (max {maxSizeMb}MB)",
|
|
@@ -113,6 +114,17 @@ export const fr = {
|
|
|
113
114
|
"sessions.preview.title": "Messages récents :",
|
|
114
115
|
"sessions.preview.you": "Vous :",
|
|
115
116
|
"sessions.preview.agent": "Agent :",
|
|
117
|
+
"attach.project_not_selected": "🏗 Aucun projet sélectionné.\n\nSélectionnez d'abord un projet avec /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Aucune session sélectionnée.\n\nSélectionnez d'abord une session avec /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ La session sélectionnée ne correspond pas au projet actuel. Sélectionnez-la de nouveau via /sessions.",
|
|
120
|
+
"attach.connected": "✅ Connecté à la session : {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Déjà connecté à la session : {title}",
|
|
122
|
+
"attach.status.idle_message": "Statut : idle. En attente de nouveaux événements.",
|
|
123
|
+
"attach.status.busy_message": "Statut : busy. Les nouveaux prompts sont temporairement bloqués.",
|
|
124
|
+
"attach.restored_question": "Une question en attente a été restaurée pour cette session.",
|
|
125
|
+
"attach.restored_permissions": "Demandes de permission en attente restaurées : {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Pour vous déconnecter, passez simplement à une autre session ou à un autre projet.",
|
|
127
|
+
"attach.error": "🔴 Impossible de se connecter à la session actuelle.",
|
|
116
128
|
"new.project_not_selected": "🏗 Aucun projet n'est sélectionné.\n\nSélectionnez d'abord un projet avec /projects.",
|
|
117
129
|
"new.created": "✅ Nouvelle session créée : {title}",
|
|
118
130
|
"new.create_error": "🔴 Le serveur OpenCode est indisponible ou une erreur s'est produite lors de la création de la session.",
|
|
@@ -232,6 +244,9 @@ export const fr = {
|
|
|
232
244
|
"pinned.line.project": "Projet : {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree : {worktree}",
|
|
234
246
|
"pinned.line.model": "Modèle : {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking : {status}",
|
|
248
|
+
"pinned.attach.status.idle": "actif, idle",
|
|
249
|
+
"pinned.attach.status.busy": "actif, busy",
|
|
235
250
|
"pinned.line.context": "Contexte : {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "Coût : {cost} dépensé",
|
|
237
252
|
"subagent.header": "Sous-agent {agent} : {description}",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -48,6 +48,7 @@ export const ru = {
|
|
|
48
48
|
"bot.prompt_send_error": "Не удалось отправить запрос в OpenCode.",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode вернул ошибку: {message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /abort для остановки.",
|
|
51
|
+
"bot.external_user_input": "Внешний ввод пользователя",
|
|
51
52
|
"bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
|
|
52
53
|
"bot.photo_downloading": "⏳ Скачиваю фото...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ Фото слишком большое (макс. {maxSizeMb}МБ)",
|
|
@@ -113,6 +114,17 @@ export const ru = {
|
|
|
113
114
|
"sessions.preview.title": "Последние сообщения:",
|
|
114
115
|
"sessions.preview.you": "Вы:",
|
|
115
116
|
"sessions.preview.agent": "Агент:",
|
|
117
|
+
"attach.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Сессия не выбрана.\n\nСначала выберите сессию через /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ Выбранная сессия не соответствует текущему проекту. Повторно выберите сессию через /sessions.",
|
|
120
|
+
"attach.connected": "✅ Подключился к сессии: {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Уже подключен к сессии: {title}",
|
|
122
|
+
"attach.status.idle_message": "Статус: idle. Жду новые события.",
|
|
123
|
+
"attach.status.busy_message": "Статус: busy. Новые промты временно заблокированы.",
|
|
124
|
+
"attach.restored_question": "Восстановил ожидающий вопрос для этой сессии.",
|
|
125
|
+
"attach.restored_permissions": "Восстановил ожидающие запросы разрешений: {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Чтобы отключиться, переключитесь на другую сессию или проект.",
|
|
127
|
+
"attach.error": "🔴 Не удалось подключиться к текущей сессии.",
|
|
116
128
|
"new.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
117
129
|
"new.created": "✅ Создана новая сессия: {title}",
|
|
118
130
|
"new.create_error": "🔴 OpenCode Server недоступен или произошла ошибка при создании сессии.",
|
|
@@ -232,6 +244,9 @@ export const ru = {
|
|
|
232
244
|
"pinned.line.project": "Проект: {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree: {worktree}",
|
|
234
246
|
"pinned.line.model": "Модель: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "активен, idle",
|
|
249
|
+
"pinned.attach.status.busy": "активен, busy",
|
|
235
250
|
"pinned.line.context": "Контекст: {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "Стоимость: {cost} потрачено",
|
|
237
252
|
"subagent.header": "Сабагент {agent}: {description}",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -48,6 +48,7 @@ export const zh = {
|
|
|
48
48
|
"bot.prompt_send_error": "向 OpenCode 发送请求失败。",
|
|
49
49
|
"bot.session_error": "🔴 OpenCode 返回错误:{message}",
|
|
50
50
|
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /abort 可停止。",
|
|
51
|
+
"bot.external_user_input": "外部用户输入",
|
|
51
52
|
"bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
|
|
52
53
|
"bot.photo_downloading": "⏳ 正在下载照片...",
|
|
53
54
|
"bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
|
|
@@ -113,6 +114,17 @@ export const zh = {
|
|
|
113
114
|
"sessions.preview.title": "最近消息:",
|
|
114
115
|
"sessions.preview.you": "你:",
|
|
115
116
|
"sessions.preview.agent": "代理:",
|
|
117
|
+
"attach.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
118
|
+
"attach.session_not_selected": "💬 未选择会话。\n\n请先使用 /sessions 选择一个会话。",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ 选中的会话与当前项目不匹配。请通过 /sessions 重新选择。",
|
|
120
|
+
"attach.connected": "✅ 已连接到会话:{title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ 已经连接到会话:{title}",
|
|
122
|
+
"attach.status.idle_message": "状态:idle。正在等待新事件。",
|
|
123
|
+
"attach.status.busy_message": "状态:busy。新的提示词暂时被阻止。",
|
|
124
|
+
"attach.restored_question": "已恢复该会话的待处理问题。",
|
|
125
|
+
"attach.restored_permissions": "已恢复待处理权限请求:{count}。",
|
|
126
|
+
"attach.disconnect_hint": "如需断开连接,只需切换到其他会话或项目。",
|
|
127
|
+
"attach.error": "🔴 无法连接到当前会话。",
|
|
116
128
|
"new.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
117
129
|
"new.created": "✅ 新会话已创建:{title}",
|
|
118
130
|
"new.create_error": "🔴 OpenCode 服务器不可用,或创建会话时发生错误。",
|
|
@@ -232,6 +244,9 @@ export const zh = {
|
|
|
232
244
|
"pinned.line.project": "项目: {project}",
|
|
233
245
|
"pinned.line.worktree": "Worktree: {worktree}",
|
|
234
246
|
"pinned.line.model": "模型: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "active, idle",
|
|
249
|
+
"pinned.attach.status.busy": "active, busy",
|
|
235
250
|
"pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
|
|
236
251
|
"pinned.line.cost": "费用: {cost}",
|
|
237
252
|
"subagent.header": "子代理 {agent}: {description}",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { interactionManager } from "./manager.js";
|
|
2
2
|
import { allowsBusyInteraction, isBusyAllowedCommand } from "./busy.js";
|
|
3
3
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
4
|
+
import { attachManager } from "../attach/manager.js";
|
|
4
5
|
function normalizeIncomingCommand(text) {
|
|
5
6
|
const trimmed = text.trim();
|
|
6
7
|
if (!trimmed.startsWith("/")) {
|
|
@@ -83,7 +84,7 @@ function isAllowedTaskCallback(ctx, state) {
|
|
|
83
84
|
export function resolveInteractionGuardDecision(ctx) {
|
|
84
85
|
const state = interactionManager.getSnapshot();
|
|
85
86
|
const { inputType, command } = classifyIncomingInput(ctx);
|
|
86
|
-
const isBusy = foregroundSessionState.isBusy();
|
|
87
|
+
const isBusy = foregroundSessionState.isBusy() || attachManager.isBusy();
|
|
87
88
|
if (state && interactionManager.isExpired()) {
|
|
88
89
|
interactionManager.clear("expired");
|
|
89
90
|
return createBlockDecision(inputType, state, "expired", command, isBusy);
|
package/dist/opencode/events.js
CHANGED
|
@@ -8,6 +8,7 @@ let eventCallback = null;
|
|
|
8
8
|
let isListening = false;
|
|
9
9
|
let activeDirectory = null;
|
|
10
10
|
let streamAbortController = null;
|
|
11
|
+
let listenerGeneration = 0;
|
|
11
12
|
function getReconnectDelayMs(attempt) {
|
|
12
13
|
const exponentialDelay = RECONNECT_BASE_DELAY_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
13
14
|
return Math.min(exponentialDelay, RECONNECT_MAX_DELAY_MS);
|
|
@@ -44,6 +45,7 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
44
45
|
activeDirectory = null;
|
|
45
46
|
}
|
|
46
47
|
const controller = new AbortController();
|
|
48
|
+
const generation = ++listenerGeneration;
|
|
47
49
|
activeDirectory = directory;
|
|
48
50
|
eventCallback = callback;
|
|
49
51
|
isListening = true;
|
|
@@ -70,7 +72,16 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
70
72
|
// Use setImmediate to avoid blocking the event loop
|
|
71
73
|
// and let grammY process incoming Telegram updates
|
|
72
74
|
const callbackSnapshot = eventCallback;
|
|
73
|
-
setImmediate(() =>
|
|
75
|
+
setImmediate(() => {
|
|
76
|
+
if (streamAbortController !== controller ||
|
|
77
|
+
controller.signal.aborted ||
|
|
78
|
+
!isListening ||
|
|
79
|
+
activeDirectory !== directory ||
|
|
80
|
+
listenerGeneration !== generation) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
callbackSnapshot(event);
|
|
84
|
+
});
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
eventStream = null;
|
|
@@ -130,6 +141,7 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
export function stopEventListening() {
|
|
144
|
+
listenerGeneration++;
|
|
133
145
|
streamAbortController?.abort();
|
|
134
146
|
streamAbortController = null;
|
|
135
147
|
isListening = false;
|