@grinev/opencode-telegram-bot 0.17.0 → 0.19.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.
Files changed (39) hide show
  1. package/.env.example +22 -1
  2. package/README.md +35 -6
  3. package/dist/app/start-bot-app.js +4 -0
  4. package/dist/attach/manager.js +66 -0
  5. package/dist/attach/service.js +171 -0
  6. package/dist/bot/commands/abort.js +0 -4
  7. package/dist/bot/commands/commands.js +13 -3
  8. package/dist/bot/commands/definitions.js +1 -0
  9. package/dist/bot/commands/mcps.js +394 -0
  10. package/dist/bot/commands/new.js +10 -18
  11. package/dist/bot/commands/opencode-start.js +2 -10
  12. package/dist/bot/commands/sessions.js +19 -20
  13. package/dist/bot/commands/start.js +2 -0
  14. package/dist/bot/handlers/prompt.js +26 -31
  15. package/dist/bot/index.js +82 -10
  16. package/dist/bot/middleware/interaction-guard.js +1 -1
  17. package/dist/bot/utils/busy-guard.js +3 -2
  18. package/dist/bot/utils/external-user-input.js +46 -0
  19. package/dist/bot/utils/switch-project.js +2 -0
  20. package/dist/config.js +25 -6
  21. package/dist/external-input/suppression.js +54 -0
  22. package/dist/i18n/de.js +34 -0
  23. package/dist/i18n/en.js +34 -0
  24. package/dist/i18n/es.js +34 -0
  25. package/dist/i18n/fr.js +34 -0
  26. package/dist/i18n/ru.js +34 -0
  27. package/dist/i18n/zh.js +34 -0
  28. package/dist/interaction/guard.js +2 -1
  29. package/dist/opencode/auto-restart.js +92 -0
  30. package/dist/opencode/events.js +13 -1
  31. package/dist/opencode/process.js +18 -1
  32. package/dist/pinned/manager.js +39 -0
  33. package/dist/summary/aggregator.js +89 -9
  34. package/dist/summary/formatter.js +2 -2
  35. package/dist/summary/markdown-to-telegram-v2.js +99 -0
  36. package/dist/summary/subagent-formatter.js +23 -2
  37. package/dist/telegram/render/inline-renderer.js +18 -0
  38. package/dist/tts/client.js +94 -19
  39. package/package.json +2 -2
package/dist/i18n/en.js CHANGED
@@ -10,6 +10,7 @@ export const en = {
10
10
  "cmd.description.tasklist": "List scheduled tasks",
11
11
  "cmd.description.commands": "Custom commands",
12
12
  "cmd.description.skills": "Skills catalog",
13
+ "cmd.description.mcps": "MCP servers",
13
14
  "cmd.description.opencode_start": "Start OpenCode server",
14
15
  "cmd.description.opencode_stop": "Stop OpenCode server",
15
16
  "cmd.description.help": "Help",
@@ -48,6 +49,7 @@ export const en = {
48
49
  "bot.prompt_send_error": "Failed to send request to OpenCode.",
49
50
  "bot.session_error": "🔴 OpenCode returned an error: {message}",
50
51
  "bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /abort to abort.",
52
+ "bot.external_user_input": "External user input",
51
53
  "bot.unknown_command": "âš ī¸ Unknown command: {command}. Use /help to see available commands.",
52
54
  "bot.photo_downloading": "âŗ Downloading photo...",
53
55
  "bot.photo_too_large": "âš ī¸ Photo is too large (max {maxSizeMb}MB)",
@@ -113,6 +115,17 @@ export const en = {
113
115
  "sessions.preview.title": "Recent messages:",
114
116
  "sessions.preview.you": "You:",
115
117
  "sessions.preview.agent": "Agent:",
118
+ "attach.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
119
+ "attach.session_not_selected": "đŸ’Ŧ Session is not selected.\n\nFirst choose a session with /sessions.",
120
+ "attach.session_project_mismatch": "âš ī¸ The selected session does not match the current project. Choose the session again via /sessions.",
121
+ "attach.connected": "✅ Connected to session: {title}",
122
+ "attach.already_connected": "â„šī¸ Already connected to session: {title}",
123
+ "attach.status.idle_message": "Status: idle. Waiting for new events.",
124
+ "attach.status.busy_message": "Status: busy. New prompts are temporarily blocked.",
125
+ "attach.restored_question": "Recovered a pending question for this session.",
126
+ "attach.restored_permissions": "Recovered pending permission requests: {count}.",
127
+ "attach.disconnect_hint": "To disconnect, switch to another session or project.",
128
+ "attach.error": "🔴 Failed to attach to the current session.",
116
129
  "new.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
117
130
  "new.created": "✅ New session created: {title}",
118
131
  "new.create_error": "🔴 OpenCode Server is unavailable or an error occurred while creating session.",
@@ -232,6 +245,9 @@ export const en = {
232
245
  "pinned.line.project": "Project: {project}",
233
246
  "pinned.line.worktree": "Worktree: {worktree}",
234
247
  "pinned.line.model": "Model: {model}",
248
+ "pinned.line.attach": "Tracking: {status}",
249
+ "pinned.attach.status.idle": "active, idle",
250
+ "pinned.attach.status.busy": "active, busy",
235
251
  "pinned.line.context": "Context: {used} / {limit} ({percent}%)",
236
252
  "pinned.line.cost": "Cost: {cost} spent",
237
253
  "subagent.header": "Subagent {agent}: {description}",
@@ -349,6 +365,24 @@ export const en = {
349
365
  "skills.button.next_page": "Next âžĄī¸",
350
366
  "skills.page_empty_callback": "No skills on this page",
351
367
  "skills.page_load_error_callback": "Cannot load this page. Please try again.",
368
+ "mcps.select": "MCP servers:",
369
+ "mcps.empty": "📭 No MCP servers configured.",
370
+ "mcps.fetch_error": "🔴 Failed to load MCP servers.",
371
+ "mcps.toggle_error": "🔴 Failed to toggle MCP server.",
372
+ "mcps.enabling": "Enabling...",
373
+ "mcps.disabling": "Disabling...",
374
+ "mcps.status.connected": "đŸŸĸ Connected",
375
+ "mcps.status.disabled": "🔴 Disabled",
376
+ "mcps.status.failed": "âš ī¸ Failed",
377
+ "mcps.status.needs_auth": "🔒 Needs auth",
378
+ "mcps.status.needs_client_registration": "🔒 Needs registration",
379
+ "mcps.detail.title": "Server: {name}",
380
+ "mcps.detail.status": "Status: {status}",
381
+ "mcps.detail.error": "Error: {error}",
382
+ "mcps.button.enable": "đŸŸĸ Enable",
383
+ "mcps.button.disable": "🔴 Disable",
384
+ "mcps.button.back": "âŦ…ī¸ Back",
385
+ "mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
352
386
  "cmd.description.rename": "Rename current session",
353
387
  "legacy.models.fetch_error": "🔴 Failed to get models list. Check server status with /status.",
354
388
  "legacy.models.empty": "📋 No available models. Configure providers in OpenCode.",
package/dist/i18n/es.js CHANGED
@@ -10,6 +10,7 @@ export const es = {
10
10
  "cmd.description.tasklist": "Ver tareas programadas",
11
11
  "cmd.description.commands": "Comandos personalizados",
12
12
  "cmd.description.skills": "CatÃĄlogo de skills",
13
+ "cmd.description.mcps": "MCP servers",
13
14
  "cmd.description.opencode_start": "Iniciar servidor OpenCode",
14
15
  "cmd.description.opencode_stop": "Detener servidor OpenCode",
15
16
  "cmd.description.help": "Ayuda",
@@ -48,6 +49,7 @@ export const es = {
48
49
  "bot.prompt_send_error": "No se pudo enviar la solicitud a OpenCode.",
49
50
  "bot.session_error": "🔴 OpenCode devolviÃŗ un error: {message}",
50
51
  "bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /abort para detenerlo.",
52
+ "bot.external_user_input": "Entrada externa del usuario",
51
53
  "bot.unknown_command": "âš ī¸ Comando desconocido: {command}. Usa /help para ver los comandos disponibles.",
52
54
  "bot.photo_downloading": "âŗ Descargando foto...",
53
55
  "bot.photo_too_large": "âš ī¸ La foto es demasiado grande (max {maxSizeMb}MB)",
@@ -113,6 +115,17 @@ export const es = {
113
115
  "sessions.preview.title": "Mensajes recientes:",
114
116
  "sessions.preview.you": "TÃē:",
115
117
  "sessions.preview.agent": "Agente:",
118
+ "attach.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
119
+ "attach.session_not_selected": "đŸ’Ŧ No hay una sesiÃŗn seleccionada.\n\nPrimero selecciona una sesiÃŗn con /sessions.",
120
+ "attach.session_project_mismatch": "âš ī¸ La sesiÃŗn seleccionada no coincide con el proyecto actual. Vuelve a seleccionarla con /sessions.",
121
+ "attach.connected": "✅ Conectado a la sesiÃŗn: {title}",
122
+ "attach.already_connected": "â„šī¸ Ya estÃĄs conectado a la sesiÃŗn: {title}",
123
+ "attach.status.idle_message": "Estado: idle. Esperando nuevos eventos.",
124
+ "attach.status.busy_message": "Estado: busy. Los nuevos prompts estÃĄn bloqueados temporalmente.",
125
+ "attach.restored_question": "Se restaurÃŗ una pregunta pendiente para esta sesiÃŗn.",
126
+ "attach.restored_permissions": "Se restauraron solicitudes de permiso pendientes: {count}.",
127
+ "attach.disconnect_hint": "Para desconectarte, cambia a otra sesiÃŗn o proyecto.",
128
+ "attach.error": "🔴 No se pudo conectar a la sesiÃŗn actual.",
116
129
  "new.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
117
130
  "new.created": "✅ SesiÃŗn nueva creada: {title}",
118
131
  "new.create_error": "🔴 OpenCode Server no estÃĄ disponible u ocurriÃŗ un error al crear la sesiÃŗn.",
@@ -232,6 +245,9 @@ export const es = {
232
245
  "pinned.line.project": "Proyecto: {project}",
233
246
  "pinned.line.worktree": "Worktree: {worktree}",
234
247
  "pinned.line.model": "Modelo: {model}",
248
+ "pinned.line.attach": "Tracking: {status}",
249
+ "pinned.attach.status.idle": "activo, idle",
250
+ "pinned.attach.status.busy": "activo, busy",
235
251
  "pinned.line.context": "Contexto: {used} / {limit} ({percent}%)",
236
252
  "pinned.line.cost": "Costo: {cost} gastado",
237
253
  "subagent.header": "Subagente {agent}: {description}",
@@ -349,6 +365,24 @@ export const es = {
349
365
  "skills.button.next_page": "Siguiente âžĄī¸",
350
366
  "skills.page_empty_callback": "No hay skills en esta pÃĄgina",
351
367
  "skills.page_load_error_callback": "No se pudo cargar esta pÃĄgina. Por favor, intÊntalo de nuevo.",
368
+ "mcps.select": "MCP servers:",
369
+ "mcps.empty": "📭 No MCP servers configured.",
370
+ "mcps.fetch_error": "🔴 Failed to load MCP servers.",
371
+ "mcps.toggle_error": "🔴 Failed to toggle MCP server.",
372
+ "mcps.enabling": "Enabling...",
373
+ "mcps.disabling": "Disabling...",
374
+ "mcps.status.connected": "đŸŸĸ Connected",
375
+ "mcps.status.disabled": "🔴 Disabled",
376
+ "mcps.status.failed": "âš ī¸ Failed",
377
+ "mcps.status.needs_auth": "🔒 Needs auth",
378
+ "mcps.status.needs_client_registration": "🔒 Needs registration",
379
+ "mcps.detail.title": "Server: {name}",
380
+ "mcps.detail.status": "Status: {status}",
381
+ "mcps.detail.error": "Error: {error}",
382
+ "mcps.button.enable": "đŸŸĸ Enable",
383
+ "mcps.button.disable": "🔴 Disable",
384
+ "mcps.button.back": "âŦ…ī¸ Back",
385
+ "mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
352
386
  "cmd.description.rename": "Renombrar la sesiÃŗn actual",
353
387
  "legacy.models.fetch_error": "🔴 No se pudo obtener la lista de modelos. Revisa el estado del servidor con /status.",
354
388
  "legacy.models.empty": "📋 No hay modelos disponibles. Configura los proveedores en OpenCode.",
package/dist/i18n/fr.js CHANGED
@@ -10,6 +10,7 @@ export const fr = {
10
10
  "cmd.description.tasklist": "Afficher les tÃĸches planifiÊes",
11
11
  "cmd.description.commands": "Commandes personnalisÊes",
12
12
  "cmd.description.skills": "Catalogue de skills",
13
+ "cmd.description.mcps": "MCP servers",
13
14
  "cmd.description.opencode_start": "DÊmarrer le serveur OpenCode",
14
15
  "cmd.description.opencode_stop": "ArrÃĒter le serveur OpenCode",
15
16
  "cmd.description.help": "Aide",
@@ -48,6 +49,7 @@ export const fr = {
48
49
  "bot.prompt_send_error": "Impossible d'envoyer la requÃĒte à OpenCode.",
49
50
  "bot.session_error": "🔴 OpenCode a renvoyÊ une erreur : {message}",
50
51
  "bot.session_retry": "🔁 {message}\n\nLe fournisseur renvoie la mÃĒme erreur à chaque nouvelle tentative. Utilisez /abort pour arrÃĒter.",
52
+ "bot.external_user_input": "EntrÊe utilisateur externe",
51
53
  "bot.unknown_command": "âš ī¸ Commande inconnue : {command}. Utilisez /help pour voir les commandes disponibles.",
52
54
  "bot.photo_downloading": "âŗ TÊlÊchargement de la photo...",
53
55
  "bot.photo_too_large": "âš ī¸ La photo est trop volumineuse (max {maxSizeMb}MB)",
@@ -113,6 +115,17 @@ export const fr = {
113
115
  "sessions.preview.title": "Messages rÊcents :",
114
116
  "sessions.preview.you": "Vous :",
115
117
  "sessions.preview.agent": "Agent :",
118
+ "attach.project_not_selected": "🏗 Aucun projet sÊlectionnÊ.\n\nSÊlectionnez d'abord un projet avec /projects.",
119
+ "attach.session_not_selected": "đŸ’Ŧ Aucune session sÊlectionnÊe.\n\nSÊlectionnez d'abord une session avec /sessions.",
120
+ "attach.session_project_mismatch": "âš ī¸ La session sÊlectionnÊe ne correspond pas au projet actuel. SÊlectionnez-la de nouveau via /sessions.",
121
+ "attach.connected": "✅ ConnectÊ à la session : {title}",
122
+ "attach.already_connected": "â„šī¸ DÊjà connectÊ à la session : {title}",
123
+ "attach.status.idle_message": "Statut : idle. En attente de nouveaux ÊvÊnements.",
124
+ "attach.status.busy_message": "Statut : busy. Les nouveaux prompts sont temporairement bloquÊs.",
125
+ "attach.restored_question": "Une question en attente a ÊtÊ restaurÊe pour cette session.",
126
+ "attach.restored_permissions": "Demandes de permission en attente restaurÊes : {count}.",
127
+ "attach.disconnect_hint": "Pour vous dÊconnecter, passez simplement à une autre session ou à un autre projet.",
128
+ "attach.error": "🔴 Impossible de se connecter à la session actuelle.",
116
129
  "new.project_not_selected": "🏗 Aucun projet n'est sÊlectionnÊ.\n\nSÊlectionnez d'abord un projet avec /projects.",
117
130
  "new.created": "✅ Nouvelle session crÊÊe : {title}",
118
131
  "new.create_error": "🔴 Le serveur OpenCode est indisponible ou une erreur s'est produite lors de la crÊation de la session.",
@@ -232,6 +245,9 @@ export const fr = {
232
245
  "pinned.line.project": "Projet : {project}",
233
246
  "pinned.line.worktree": "Worktree : {worktree}",
234
247
  "pinned.line.model": "Modèle : {model}",
248
+ "pinned.line.attach": "Tracking : {status}",
249
+ "pinned.attach.status.idle": "actif, idle",
250
+ "pinned.attach.status.busy": "actif, busy",
235
251
  "pinned.line.context": "Contexte : {used} / {limit} ({percent}%)",
236
252
  "pinned.line.cost": "CoÃģt : {cost} dÊpensÊ",
237
253
  "subagent.header": "Sous-agent {agent} : {description}",
@@ -349,6 +365,24 @@ export const fr = {
349
365
  "skills.button.next_page": "Suivant âžĄī¸",
350
366
  "skills.page_empty_callback": "Aucun skill sur cette page",
351
367
  "skills.page_load_error_callback": "Impossible de charger cette page. Veuillez rÊessayer.",
368
+ "mcps.select": "MCP servers:",
369
+ "mcps.empty": "📭 No MCP servers configured.",
370
+ "mcps.fetch_error": "🔴 Failed to load MCP servers.",
371
+ "mcps.toggle_error": "🔴 Failed to toggle MCP server.",
372
+ "mcps.enabling": "Enabling...",
373
+ "mcps.disabling": "Disabling...",
374
+ "mcps.status.connected": "đŸŸĸ Connected",
375
+ "mcps.status.disabled": "🔴 Disabled",
376
+ "mcps.status.failed": "âš ī¸ Failed",
377
+ "mcps.status.needs_auth": "🔒 Needs auth",
378
+ "mcps.status.needs_client_registration": "🔒 Needs registration",
379
+ "mcps.detail.title": "Server: {name}",
380
+ "mcps.detail.status": "Status: {status}",
381
+ "mcps.detail.error": "Error: {error}",
382
+ "mcps.button.enable": "đŸŸĸ Enable",
383
+ "mcps.button.disable": "🔴 Disable",
384
+ "mcps.button.back": "âŦ…ī¸ Back",
385
+ "mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
352
386
  "cmd.description.rename": "Renommer la session actuelle",
353
387
  "legacy.models.fetch_error": "🔴 Impossible de rÊcupÊrer la liste des modèles. VÊrifiez l'Êtat du serveur avec /status.",
354
388
  "legacy.models.empty": "📋 Aucun modèle disponible. Configurez les fournisseurs dans OpenCode.",
package/dist/i18n/ru.js CHANGED
@@ -10,6 +10,7 @@ export const ru = {
10
10
  "cmd.description.tasklist": "ĐĄĐŋĐ¸ŅĐžĐē ĐˇĐ°Đ´Đ°Ņ‡ ĐŋĐž Ņ€Đ°ŅĐŋĐ¸ŅĐ°ĐŊĐ¸ŅŽ",
11
11
  "cmd.description.commands": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌҁĐēиĐĩ ĐēĐžĐŧаĐŊĐ´Ņ‹",
12
12
  "cmd.description.skills": "ĐšĐ°Ņ‚Đ°ĐģĐžĐŗ ҁĐēиĐģĐģОв",
13
+ "cmd.description.mcps": "MCP ҁĐĩŅ€Đ˛ĐĩҀҋ",
13
14
  "cmd.description.opencode_start": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚ŅŒ OpenCode ҁĐĩŅ€Đ˛ĐĩŅ€",
14
15
  "cmd.description.opencode_stop": "ĐžŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ OpenCode ҁĐĩŅ€Đ˛ĐĩŅ€",
15
16
  "cmd.description.help": "ĐĄĐŋŅ€Đ°Đ˛Đēа",
@@ -48,6 +49,7 @@ export const ru = {
48
49
  "bot.prompt_send_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ СаĐŋŅ€ĐžŅ в OpenCode.",
49
50
  "bot.session_error": "🔴 OpenCode вĐĩŅ€ĐŊ҃Đģ ĐžŅˆĐ¸ĐąĐē҃: {message}",
50
51
  "bot.session_retry": "🔁 {message}\n\nĐŸŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ Đ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‰Đ°ĐĩŅ‚ ОдĐŊ҃ и Ņ‚Ņƒ ĐļĐĩ ĐžŅˆĐ¸ĐąĐē҃ ĐŋŅ€Đ¸ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊҋ҅ СаĐŋŅ€ĐžŅĐ°Ņ…. Đ˜ŅĐŋĐžĐģŅŒĐˇŅƒĐšŅ‚Đĩ /abort Đ´ĐģŅ ĐžŅŅ‚Đ°ĐŊОвĐēи.",
52
+ "bot.external_user_input": "ВĐŊĐĩ҈ĐŊиК ввОд ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ",
51
53
  "bot.unknown_command": "âš ī¸ НĐĩиСвĐĩҁ҂ĐŊĐ°Ņ ĐēĐžĐŧаĐŊда: {command}. Đ˜ŅĐŋĐžĐģŅŒĐˇŅƒĐšŅ‚Đĩ /help Đ´ĐģŅ ҁĐŋĐ¸ŅĐēа ĐēĐžĐŧаĐŊĐ´.",
52
54
  "bot.photo_downloading": "âŗ ĐĄĐēĐ°Ņ‡Đ¸Đ˛Đ°ŅŽ Ņ„ĐžŅ‚Đž...",
53
55
  "bot.photo_too_large": "âš ī¸ Đ¤ĐžŅ‚Đž ҁĐģĐ¸ŅˆĐēĐžĐŧ йОĐģŅŒŅˆĐžĐĩ (ĐŧаĐēҁ. {maxSizeMb}МБ)",
@@ -113,6 +115,17 @@ export const ru = {
113
115
  "sessions.preview.title": "ĐŸĐžŅĐģĐĩĐ´ĐŊиĐĩ ŅĐžĐžĐąŅ‰ĐĩĐŊĐ¸Ņ:",
114
116
  "sessions.preview.you": "Đ’Ņ‹:",
115
117
  "sessions.preview.agent": "ĐĐŗĐĩĐŊŅ‚:",
118
+ "attach.project_not_selected": "🏗 ĐŸŅ€ĐžĐĩĐēŅ‚ ĐŊĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊ.\n\nĐĄĐŊĐ°Ņ‡Đ°Đģа Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐŋŅ€ĐžĐĩĐēŅ‚ ĐēĐžĐŧаĐŊдОК /projects.",
119
+ "attach.session_not_selected": "đŸ’Ŧ ĐĄĐĩŅŅĐ¸Ņ ĐŊĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊа.\n\nĐĄĐŊĐ°Ņ‡Đ°Đģа Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ҁĐĩŅŅĐ¸ŅŽ ҇ĐĩŅ€ĐĩС /sessions.",
120
+ "attach.session_project_mismatch": "âš ī¸ Đ’Ņ‹ĐąŅ€Đ°ĐŊĐŊĐ°Ņ ҁĐĩŅŅĐ¸Ņ ĐŊĐĩ ŅĐžĐžŅ‚Đ˛ĐĩŅ‚ŅŅ‚Đ˛ŅƒĐĩŅ‚ Ņ‚ĐĩĐēŅƒŅ‰ĐĩĐŧ҃ ĐŋŅ€ĐžĐĩĐēŅ‚Ņƒ. ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ҁĐĩŅŅĐ¸ŅŽ ҇ĐĩŅ€ĐĩС /sessions.",
121
+ "attach.connected": "✅ ПодĐēĐģŅŽŅ‡Đ¸ĐģŅŅ Đē ҁĐĩŅŅĐ¸Đ¸: {title}",
122
+ "attach.already_connected": "â„šī¸ ĐŖĐļĐĩ ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊ Đē ҁĐĩŅŅĐ¸Đ¸: {title}",
123
+ "attach.status.idle_message": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ: idle. Đ–Đ´Ņƒ ĐŊĐžĐ˛Ņ‹Đĩ ŅĐžĐąŅ‹Ņ‚Đ¸Ņ.",
124
+ "attach.status.busy_message": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ: busy. ĐĐžĐ˛Ņ‹Đĩ ĐŋŅ€ĐžĐŧ҂ҋ Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊĐž СайĐģĐžĐēĐ¸Ņ€ĐžĐ˛Đ°ĐŊŅ‹.",
125
+ "attach.restored_question": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвиĐģ ĐžĐļĐ¸Đ´Đ°ŅŽŅ‰Đ¸Đš вОĐŋŅ€ĐžŅ Đ´ĐģŅ ŅŅ‚ĐžĐš ҁĐĩŅŅĐ¸Đ¸.",
126
+ "attach.restored_permissions": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвиĐģ ĐžĐļĐ¸Đ´Đ°ŅŽŅ‰Đ¸Đĩ СаĐŋŅ€ĐžŅŅ‹ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊиК: {count}.",
127
+ "attach.disconnect_hint": "Đ§Ņ‚ĐžĐąŅ‹ ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ, ĐŋĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ¸Ņ‚ĐĩҁҌ ĐŊа Đ´Ņ€ŅƒĐŗŅƒŅŽ ҁĐĩŅŅĐ¸ŅŽ иĐģи ĐŋŅ€ĐžĐĩĐēŅ‚.",
128
+ "attach.error": "🔴 НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋОдĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Đē Ņ‚ĐĩĐēŅƒŅ‰ĐĩĐš ҁĐĩŅŅĐ¸Đ¸.",
116
129
  "new.project_not_selected": "🏗 ĐŸŅ€ĐžĐĩĐēŅ‚ ĐŊĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊ.\n\nĐĄĐŊĐ°Ņ‡Đ°Đģа Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐŋŅ€ĐžĐĩĐēŅ‚ ĐēĐžĐŧаĐŊдОК /projects.",
117
130
  "new.created": "✅ ХОСдаĐŊа ĐŊĐžĐ˛Đ°Ņ ҁĐĩŅŅĐ¸Ņ: {title}",
118
131
  "new.create_error": "🔴 OpenCode Server ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ иĐģи ĐŋŅ€ĐžĐ¸ĐˇĐžŅˆĐģа ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ŅĐžĐˇĐ´Đ°ĐŊии ҁĐĩŅŅĐ¸Đ¸.",
@@ -232,6 +245,9 @@ export const ru = {
232
245
  "pinned.line.project": "ĐŸŅ€ĐžĐĩĐēŅ‚: {project}",
233
246
  "pinned.line.worktree": "Worktree: {worktree}",
234
247
  "pinned.line.model": "МодĐĩĐģҌ: {model}",
248
+ "pinned.line.attach": "Tracking: {status}",
249
+ "pinned.attach.status.idle": "аĐēŅ‚Đ¸Đ˛ĐĩĐŊ, idle",
250
+ "pinned.attach.status.busy": "аĐēŅ‚Đ¸Đ˛ĐĩĐŊ, busy",
235
251
  "pinned.line.context": "КоĐŊŅ‚ĐĩĐēҁ҂: {used} / {limit} ({percent}%)",
236
252
  "pinned.line.cost": "ĐĄŅ‚ĐžĐ¸ĐŧĐžŅŅ‚ŅŒ: {cost} ĐŋĐžŅ‚Ņ€Đ°Ņ‡ĐĩĐŊĐž",
237
253
  "subagent.header": "ĐĄĐ°ĐąĐ°ĐŗĐĩĐŊŅ‚ {agent}: {description}",
@@ -349,6 +365,24 @@ export const ru = {
349
365
  "skills.button.next_page": "ВĐŋĐĩŅ€Ņ‘Đ´ âžĄī¸",
350
366
  "skills.page_empty_callback": "На ŅŅ‚ĐžĐš ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đĩ ĐŊĐĩŅ‚ ҁĐēиĐģĐģОв",
351
367
  "skills.page_load_error_callback": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ŅŅ‚Ņƒ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ĐŋĐžĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ҁĐŊОва.",
368
+ "mcps.select": "MCP ҁĐĩŅ€Đ˛ĐĩҀҋ:",
369
+ "mcps.empty": "📭 MCP ҁĐĩŅ€Đ˛ĐĩҀҋ ĐŊĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐĩĐŊŅ‹.",
370
+ "mcps.fetch_error": "🔴 НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ MCP ҁĐĩŅ€Đ˛ĐĩҀҋ.",
371
+ "mcps.toggle_error": "🔴 НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ MCP ҁĐĩŅ€Đ˛ĐĩŅ€.",
372
+ "mcps.enabling": "ВĐēĐģŅŽŅ‡Đ°ŅŽ...",
373
+ "mcps.disabling": "Đ’Ņ‹ĐēĐģŅŽŅ‡Đ°ŅŽ...",
374
+ "mcps.status.connected": "đŸŸĸ ПодĐēĐģŅŽŅ‡ĐĩĐŊ",
375
+ "mcps.status.disabled": "🔴 ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊ",
376
+ "mcps.status.failed": "âš ī¸ ĐžŅˆĐ¸ĐąĐēа",
377
+ "mcps.status.needs_auth": "🔒 ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Đ¸Ņ",
378
+ "mcps.status.needs_client_registration": "🔒 ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ",
379
+ "mcps.detail.title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€: {name}",
380
+ "mcps.detail.status": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ: {status}",
381
+ "mcps.detail.error": "ĐžŅˆĐ¸ĐąĐēа: {error}",
382
+ "mcps.button.enable": "đŸŸĸ ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ",
383
+ "mcps.button.disable": "🔴 Đ’Ņ‹ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ",
384
+ "mcps.button.back": "âŦ…ī¸ Назад",
385
+ "mcps.auth_required": "Đ­Ņ‚ĐžŅ‚ ҁĐĩŅ€Đ˛ĐĩŅ€ ҂ҀĐĩĐąŅƒĐĩŅ‚ Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Đ¸Đ¸ и ĐŊĐĩ ĐŧĐžĐļĐĩŅ‚ ĐąŅ‹Ņ‚ŅŒ вĐēĐģŅŽŅ‡ĐĩĐŊ иС ĐąĐžŅ‚Đ°.",
352
386
  "cmd.description.rename": "ПĐĩŅ€ĐĩиĐŧĐĩĐŊĐžĐ˛Đ°Ņ‚ŅŒ Ņ‚ĐĩĐēŅƒŅ‰ŅƒŅŽ ҁĐĩŅŅĐ¸ŅŽ",
353
387
  "legacy.models.fetch_error": "🔴 НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ҁĐŋĐ¸ŅĐžĐē ĐŧОдĐĩĐģĐĩĐš. ĐŸŅ€ĐžĐ˛ĐĩŅ€ŅŒŅ‚Đĩ ŅŅ‚Đ°Ņ‚ŅƒŅ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° /status.",
354
388
  "legacy.models.empty": "📋 НĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊҋ҅ ĐŧОдĐĩĐģĐĩĐš. ĐĐ°ŅŅ‚Ņ€ĐžĐšŅ‚Đĩ ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩҀҋ ҇ĐĩŅ€ĐĩС OpenCode.",
package/dist/i18n/zh.js CHANGED
@@ -10,6 +10,7 @@ export const zh = {
10
10
  "cmd.description.tasklist": "æŸĨįœ‹åŽšæ—ļäģģåŠĄ",
11
11
  "cmd.description.commands": "č‡Ē厚䚉å‘Ŋäģ¤",
12
12
  "cmd.description.skills": "技čƒŊį›ŽåŊ•",
13
+ "cmd.description.mcps": "MCP servers",
13
14
  "cmd.description.opencode_start": "启动 OpenCode æœåŠĄå™¨",
14
15
  "cmd.description.opencode_stop": "停æ­ĸ OpenCode æœåŠĄå™¨",
15
16
  "cmd.description.help": "帎劊",
@@ -48,6 +49,7 @@ export const zh = {
48
49
  "bot.prompt_send_error": "向 OpenCode å‘é€č¯ˇæą‚å¤ąč´Ĩ。",
49
50
  "bot.session_error": "🔴 OpenCode čŋ”å›žé”™č¯¯īŧš{message}",
50
51
  "bot.session_retry": "🔁 {message}\n\næäž›æ–šåœ¨é‡å¤é‡č¯•æ—ļ持įģ­čŋ”å›žåŒä¸€é”™č¯¯ã€‚äŊŋᔍ /abort 可停æ­ĸ。",
52
+ "bot.external_user_input": "å¤–éƒ¨į”¨æˆˇčž“å…Ĩ",
51
53
  "bot.unknown_command": "âš ī¸ æœĒįŸĨå‘Ŋäģ¤īŧš{command}。äŊŋᔍ /help æŸĨįœ‹å¯į”¨å‘Ŋäģ¤ã€‚",
52
54
  "bot.photo_downloading": "âŗ æ­Ŗåœ¨ä¸‹čŊŊᅧቇ...",
53
55
  "bot.photo_too_large": "âš ī¸ ᅧቇčŋ‡å¤§īŧˆæœ€å¤§ {maxSizeMb}MBīŧ‰",
@@ -113,6 +115,17 @@ export const zh = {
113
115
  "sessions.preview.title": "最čŋ‘æļˆæ¯īŧš",
114
116
  "sessions.preview.you": "äŊ īŧš",
115
117
  "sessions.preview.agent": "äģŖį†īŧš",
118
+ "attach.project_not_selected": "🏗 æœĒé€‰æ‹ŠéĄšį›Žã€‚\n\nč¯ˇå…ˆäŊŋᔍ /projects 选拊一ä¸ĒéĄšį›Žã€‚",
119
+ "attach.session_not_selected": "đŸ’Ŧ æœĒ选拊äŧšč¯ã€‚\n\nč¯ˇå…ˆäŊŋᔍ /sessions 选拊一ä¸Ēäŧšč¯ã€‚",
120
+ "attach.session_project_mismatch": "âš ī¸ é€‰ä¸­įš„äŧšč¯ä¸ŽåŊ“å‰éĄšį›Žä¸åŒšé…ã€‚č¯ˇé€ščŋ‡ /sessions 重新选拊。",
121
+ "attach.connected": "✅ 厞čŋžæŽĨ到äŧšč¯īŧš{title}",
122
+ "attach.already_connected": "â„šī¸ 厞įģčŋžæŽĨ到äŧšč¯īŧš{title}",
123
+ "attach.status.idle_message": "įŠļ态īŧšidleã€‚æ­Ŗåœ¨į­‰åž…æ–°äē‹äģļ。",
124
+ "attach.status.busy_message": "įŠļ态īŧšbusyã€‚æ–°įš„æį¤ēč¯æš‚æ—ļčĸĢé˜ģæ­ĸ。",
125
+ "attach.restored_question": "厞æĸ复č¯Ĩäŧšč¯įš„åž…å¤„į†é—Žéĸ˜ã€‚",
126
+ "attach.restored_permissions": "厞æĸå¤åž…å¤„į†æƒé™č¯ˇæą‚īŧš{count}。",
127
+ "attach.disconnect_hint": "åĻ‚éœ€æ–­åŧ€čŋžæŽĨīŧŒåĒ需切æĸ到å…ļäģ–äŧšč¯æˆ–éĄšį›Žã€‚",
128
+ "attach.error": "🔴 æ— æŗ•čŋžæŽĨ到åŊ“前äŧšč¯ã€‚",
116
129
  "new.project_not_selected": "🏗 æœĒé€‰æ‹ŠéĄšį›Žã€‚\n\nč¯ˇå…ˆäŊŋᔍ /projects 选拊一ä¸ĒéĄšį›Žã€‚",
117
130
  "new.created": "✅ 新äŧšč¯åˇ˛åˆ›åģēīŧš{title}",
118
131
  "new.create_error": "🔴 OpenCode æœåŠĄå™¨ä¸å¯į”¨īŧŒæˆ–创åģēäŧšč¯æ—ļå‘į”Ÿé”™č¯¯ã€‚",
@@ -232,6 +245,9 @@ export const zh = {
232
245
  "pinned.line.project": "éĄšį›Ž: {project}",
233
246
  "pinned.line.worktree": "Worktree: {worktree}",
234
247
  "pinned.line.model": "æ¨Ąåž‹: {model}",
248
+ "pinned.line.attach": "Tracking: {status}",
249
+ "pinned.attach.status.idle": "active, idle",
250
+ "pinned.attach.status.busy": "active, busy",
235
251
  "pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
236
252
  "pinned.line.cost": "č´šį”¨: {cost}",
237
253
  "subagent.header": "子äģŖį† {agent}: {description}",
@@ -349,6 +365,24 @@ export const zh = {
349
365
  "skills.button.next_page": "下一éĄĩ âžĄī¸",
350
366
  "skills.page_empty_callback": "čŋ™ä¸€éĄĩæ˛Ąæœ‰æŠ€čƒŊ",
351
367
  "skills.page_load_error_callback": "æ— æŗ•åŠ čŊŊæ­¤éĄĩéĸã€‚č¯ˇé‡č¯•ã€‚",
368
+ "mcps.select": "MCP servers:",
369
+ "mcps.empty": "📭 No MCP servers configured.",
370
+ "mcps.fetch_error": "🔴 Failed to load MCP servers.",
371
+ "mcps.toggle_error": "🔴 Failed to toggle MCP server.",
372
+ "mcps.enabling": "Enabling...",
373
+ "mcps.disabling": "Disabling...",
374
+ "mcps.status.connected": "đŸŸĸ Connected",
375
+ "mcps.status.disabled": "🔴 Disabled",
376
+ "mcps.status.failed": "âš ī¸ Failed",
377
+ "mcps.status.needs_auth": "🔒 Needs auth",
378
+ "mcps.status.needs_client_registration": "🔒 Needs registration",
379
+ "mcps.detail.title": "Server: {name}",
380
+ "mcps.detail.status": "Status: {status}",
381
+ "mcps.detail.error": "Error: {error}",
382
+ "mcps.button.enable": "đŸŸĸ Enable",
383
+ "mcps.button.disable": "🔴 Disable",
384
+ "mcps.button.back": "âŦ…ī¸ Back",
385
+ "mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
352
386
  "cmd.description.rename": "重å‘Ŋ名åŊ“前äŧšč¯",
353
387
  "legacy.models.fetch_error": "🔴 čŽˇå–æ¨Ąåž‹åˆ—čĄ¨å¤ąč´Ĩã€‚č¯ˇäŊŋᔍ /status æŖ€æŸĨæœåŠĄå™¨įŠļ态。",
354
388
  "legacy.models.empty": "📋 æ˛Ąæœ‰å¯į”¨æ¨Ąåž‹ã€‚č¯ˇåœ¨ OpenCode 中配įŊŽ providers。",
@@ -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);
@@ -0,0 +1,92 @@
1
+ import { config } from "../config.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { opencodeClient } from "./client.js";
4
+ import { resolveLocalOpencodeTarget, startLocalOpencodeServer, } from "./process.js";
5
+ const SERVER_READY_TIMEOUT_MS = 10000;
6
+ const SERVER_READY_POLL_INTERVAL_MS = 500;
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ async function isOpencodeServerHealthy() {
11
+ try {
12
+ const { data, error } = await opencodeClient.global.health();
13
+ return !error && data?.healthy === true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ async function waitForOpencodeServerReady(timeoutMs) {
20
+ const startedAt = Date.now();
21
+ while (Date.now() - startedAt < timeoutMs) {
22
+ if (await isOpencodeServerHealthy()) {
23
+ return true;
24
+ }
25
+ await sleep(SERVER_READY_POLL_INTERVAL_MS);
26
+ }
27
+ return false;
28
+ }
29
+ export class OpencodeAutoRestartService {
30
+ timer = null;
31
+ localTarget = null;
32
+ started = false;
33
+ checkInProgress = false;
34
+ async start() {
35
+ if (this.started || !config.opencode.autoRestartEnabled) {
36
+ return;
37
+ }
38
+ const localTarget = resolveLocalOpencodeTarget(config.opencode.apiUrl);
39
+ if (!localTarget) {
40
+ logger.warn(`[OpenCodeAutoRestart] Disabled because OPENCODE_API_URL is not local: ${config.opencode.apiUrl}`);
41
+ return;
42
+ }
43
+ this.started = true;
44
+ this.localTarget = localTarget;
45
+ logger.info(`[OpenCodeAutoRestart] Enabled: port=${localTarget.port}, intervalSec=${config.opencode.monitorIntervalSec}`);
46
+ await this.checkAndRestart("startup");
47
+ this.timer = setInterval(() => {
48
+ void this.checkAndRestart("interval");
49
+ }, config.opencode.monitorIntervalSec * 1000);
50
+ this.timer.unref?.();
51
+ }
52
+ stop() {
53
+ if (this.timer) {
54
+ clearInterval(this.timer);
55
+ this.timer = null;
56
+ }
57
+ this.started = false;
58
+ this.localTarget = null;
59
+ }
60
+ async checkAndRestart(reason) {
61
+ if (this.checkInProgress || !this.localTarget) {
62
+ return;
63
+ }
64
+ this.checkInProgress = true;
65
+ try {
66
+ if (await isOpencodeServerHealthy()) {
67
+ logger.debug(`[OpenCodeAutoRestart] Health-check succeeded: reason=${reason}`);
68
+ return;
69
+ }
70
+ logger.warn(`[OpenCodeAutoRestart] OpenCode server is unavailable, starting local server: reason=${reason}, port=${this.localTarget.port}`);
71
+ const childProcess = startLocalOpencodeServer(this.localTarget);
72
+ childProcess.once("error", (error) => {
73
+ logger.error("[OpenCodeAutoRestart] OpenCode server process failed to start", error);
74
+ });
75
+ const pid = childProcess.pid;
76
+ childProcess.unref();
77
+ const ready = await waitForOpencodeServerReady(SERVER_READY_TIMEOUT_MS);
78
+ if (!ready) {
79
+ logger.warn(`[OpenCodeAutoRestart] OpenCode server was started but did not become ready: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
80
+ return;
81
+ }
82
+ logger.info(`[OpenCodeAutoRestart] OpenCode server recovered: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
83
+ }
84
+ catch (error) {
85
+ logger.error("[OpenCodeAutoRestart] Failed to check or restart OpenCode server", error);
86
+ }
87
+ finally {
88
+ this.checkInProgress = false;
89
+ }
90
+ }
91
+ }
92
+ export const opencodeAutoRestartService = new OpencodeAutoRestartService();
@@ -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(() => callbackSnapshot(event));
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;
@@ -1,4 +1,4 @@
1
- import { exec } from "node:child_process";
1
+ import { exec, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  const execAsync = promisify(exec);
4
4
  const DEFAULT_OPENCODE_PORT = 4096;
@@ -25,6 +25,23 @@ export function resolveLocalOpencodeTarget(apiUrl) {
25
25
  return null;
26
26
  }
27
27
  }
28
+ export function createOpencodeServeSpawnCommand(target) {
29
+ const isWindows = process.platform === "win32";
30
+ const port = target.port.toString();
31
+ return {
32
+ command: isWindows ? "cmd.exe" : "opencode",
33
+ args: isWindows ? ["/c", "opencode", "serve", "--port", port] : ["serve", "--port", port],
34
+ windowsHide: isWindows,
35
+ };
36
+ }
37
+ export function startLocalOpencodeServer(target) {
38
+ const spawnCommand = createOpencodeServeSpawnCommand(target);
39
+ return spawn(spawnCommand.command, spawnCommand.args, {
40
+ detached: true,
41
+ stdio: "ignore",
42
+ windowsHide: spawnCommand.windowsHide,
43
+ });
44
+ }
28
45
  function parsePid(value) {
29
46
  const pid = Number.parseInt(value.trim(), 10);
30
47
  return Number.isInteger(pid) && pid > 0 ? pid : null;
@@ -15,6 +15,8 @@ class PinnedMessageManager {
15
15
  chatId: null,
16
16
  sessionId: null,
17
17
  sessionTitle: t("pinned.default_session_title"),
18
+ attachActive: false,
19
+ attachBusy: false,
18
20
  projectPath: "",
19
21
  projectBranch: null,
20
22
  projectWorktreePath: null,
@@ -55,6 +57,8 @@ class PinnedMessageManager {
55
57
  // Update state
56
58
  this.state.sessionId = sessionId;
57
59
  this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
60
+ this.state.attachActive = false;
61
+ this.state.attachBusy = false;
58
62
  await this.refreshProjectMetadata();
59
63
  // Fetch context limit for current model
60
64
  await this.fetchContextLimit();
@@ -73,6 +77,27 @@ class PinnedMessageManager {
73
77
  // Load existing diffs from API (for session restoration)
74
78
  await this.loadDiffsFromApi(sessionId);
75
79
  }
80
+ /**
81
+ * Restore in-memory state for a persisted pinned message without creating a new Telegram message.
82
+ */
83
+ async restoreExistingSession(sessionId, sessionTitle) {
84
+ logger.info(`[PinnedManager] Restoring existing pinned message for session: ${sessionId}`);
85
+ this.state.sessionId = sessionId;
86
+ this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
87
+ this.state.attachActive = false;
88
+ this.state.attachBusy = false;
89
+ this.state.changedFiles = [];
90
+ this.lastRenderedMessageText = null;
91
+ this.pendingUpdate = false;
92
+ this.pendingForceUpdate = false;
93
+ await this.refreshProjectMetadata();
94
+ await this.fetchContextLimit();
95
+ if (this.onKeyboardUpdateCallback && this.state.tokensLimit > 0) {
96
+ this.onKeyboardUpdateCallback(this.state.tokensUsed, this.state.tokensLimit);
97
+ }
98
+ await this.updatePinnedMessage(true);
99
+ await this.loadDiffsFromApi(sessionId);
100
+ }
76
101
  /**
77
102
  * Called when session title is updated (after first message)
78
103
  */
@@ -83,6 +108,15 @@ class PinnedMessageManager {
83
108
  await this.updatePinnedMessage();
84
109
  }
85
110
  }
111
+ async setAttachState(active, busy) {
112
+ const nextBusy = active ? busy : false;
113
+ if (this.state.attachActive === active && this.state.attachBusy === nextBusy) {
114
+ return;
115
+ }
116
+ this.state.attachActive = active;
117
+ this.state.attachBusy = nextBusy;
118
+ await this.updatePinnedMessage();
119
+ }
86
120
  /**
87
121
  * Load context token usage from session history
88
122
  */
@@ -672,6 +706,9 @@ class PinnedMessageManager {
672
706
  // Just reset state if not initialized
673
707
  this.state.messageId = null;
674
708
  this.state.sessionId = null;
709
+ this.state.sessionTitle = t("pinned.default_session_title");
710
+ this.state.attachActive = false;
711
+ this.state.attachBusy = false;
675
712
  this.state.tokensUsed = 0;
676
713
  this.state.tokensLimit = 0;
677
714
  this.state.projectPath = "";
@@ -691,6 +728,8 @@ class PinnedMessageManager {
691
728
  this.state.messageId = null;
692
729
  this.state.sessionId = null;
693
730
  this.state.sessionTitle = t("pinned.default_session_title");
731
+ this.state.attachActive = false;
732
+ this.state.attachBusy = false;
694
733
  this.state.projectPath = "";
695
734
  this.state.projectBranch = null;
696
735
  this.state.projectWorktreePath = null;