@grinev/opencode-telegram-bot 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +25 -0
- package/README.md +50 -26
- package/dist/agent/manager.js +34 -6
- package/dist/agent/types.js +7 -1
- package/dist/app/start-bot-app.js +6 -1
- package/dist/bot/assistant-run-state.js +60 -0
- package/dist/bot/commands/abort.js +2 -0
- package/dist/bot/commands/commands.js +12 -2
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/new.js +3 -2
- package/dist/bot/commands/open.js +314 -0
- package/dist/bot/commands/projects.js +5 -37
- package/dist/bot/commands/sessions.js +3 -0
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/commands/status.js +5 -2
- package/dist/bot/commands/tts.js +13 -0
- package/dist/bot/handlers/agent.js +3 -4
- package/dist/bot/handlers/inline-menu.js +9 -1
- package/dist/bot/handlers/model.js +3 -2
- package/dist/bot/handlers/prompt.js +33 -5
- package/dist/bot/handlers/variant.js +3 -2
- package/dist/bot/index.js +179 -94
- package/dist/bot/message-patterns.js +2 -2
- package/dist/bot/streaming/response-streamer.js +26 -17
- package/dist/bot/streaming/tool-call-streamer.js +31 -24
- package/dist/bot/utils/assistant-rendering.js +117 -0
- package/dist/bot/utils/assistant-run-footer.js +9 -0
- package/dist/bot/utils/browser-roots.js +140 -0
- package/dist/bot/utils/file-tree.js +92 -0
- package/dist/bot/utils/finalize-assistant-response.js +18 -24
- package/dist/bot/utils/keyboard.js +6 -6
- package/dist/bot/utils/send-tts-response.js +37 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +3 -3
- package/dist/bot/utils/switch-project.js +48 -0
- package/dist/bot/utils/telegram-text.js +95 -1
- package/dist/cli.js +4 -0
- package/dist/config.js +10 -0
- package/dist/i18n/de.js +32 -9
- package/dist/i18n/en.js +32 -9
- package/dist/i18n/es.js +32 -9
- package/dist/i18n/fr.js +32 -9
- package/dist/i18n/ru.js +32 -9
- package/dist/i18n/zh.js +32 -9
- package/dist/index.js +2 -0
- package/dist/pinned/manager.js +66 -4
- package/dist/project/manager.js +2 -1
- package/dist/settings/manager.js +7 -0
- package/dist/summary/aggregator.js +17 -1
- package/dist/summary/formatter.js +12 -89
- package/dist/telegram/render/block-fallback.js +28 -0
- package/dist/telegram/render/block-parser.js +295 -0
- package/dist/telegram/render/block-renderer.js +457 -0
- package/dist/telegram/render/chunker.js +281 -0
- package/dist/telegram/render/inline-renderer.js +128 -0
- package/dist/telegram/render/markdown-normalizer.js +94 -0
- package/dist/telegram/render/pipeline.js +9 -0
- package/dist/telegram/render/types.js +1 -0
- package/dist/telegram/render/validator.js +160 -0
- package/dist/tts/client.js +59 -0
- package/dist/utils/logger.js +200 -73
- package/package.json +6 -2
package/dist/i18n/ru.js
CHANGED
|
@@ -3,6 +3,7 @@ export const ru = {
|
|
|
3
3
|
"cmd.description.new": "Создать новую сессию",
|
|
4
4
|
"cmd.description.stop": "Прервать текущее действие",
|
|
5
5
|
"cmd.description.sessions": "Список сессий",
|
|
6
|
+
"cmd.description.tts": "Переключить аудиоответы",
|
|
6
7
|
"cmd.description.projects": "Список проектов",
|
|
7
8
|
"cmd.description.task": "Создать задачу по расписанию",
|
|
8
9
|
"cmd.description.tasklist": "Список задач по расписанию",
|
|
@@ -32,8 +33,8 @@ export const ru = {
|
|
|
32
33
|
"inline.cancelled_callback": "Отменено",
|
|
33
34
|
"common.unknown": "неизвестна",
|
|
34
35
|
"common.unknown_error": "неизвестная ошибка",
|
|
35
|
-
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\n
|
|
36
|
-
"help.keyboard_hint": "💡
|
|
36
|
+
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\nАгент, модель и вариант выбираются кнопками внизу.",
|
|
37
|
+
"help.keyboard_hint": "💡 Агент, модель, вариант и действия с контекстом доступны через нижние кнопки клавиатуры.",
|
|
37
38
|
"help.text": "📖 **Справка**\n\n/status - Проверить статус сервера\n/sessions - Список сессий\n/new - Создать новую сессию\n/help - Справка",
|
|
38
39
|
"bot.thinking": "💭 Думаю...",
|
|
39
40
|
"bot.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
@@ -65,8 +66,11 @@ export const ru = {
|
|
|
65
66
|
"status.line.managed_no": "Запущен ботом: Нет",
|
|
66
67
|
"status.line.pid": "PID: {pid}",
|
|
67
68
|
"status.line.uptime_sec": "Uptime: {seconds} сек",
|
|
68
|
-
"status.line.mode": "
|
|
69
|
+
"status.line.mode": "Агент: {mode}",
|
|
69
70
|
"status.line.model": "Модель: {model}",
|
|
71
|
+
"status.line.tts": "TTS-ответы: {tts}",
|
|
72
|
+
"status.tts.on": "Вкл",
|
|
73
|
+
"status.tts.off": "Выкл",
|
|
70
74
|
"status.agent_not_set": "не установлен",
|
|
71
75
|
"status.project_selected": "Проект: {project}",
|
|
72
76
|
"status.project_not_selected": "Проект: не выбран",
|
|
@@ -75,6 +79,10 @@ export const ru = {
|
|
|
75
79
|
"status.session_not_selected": "Текущая сессия: не выбрана",
|
|
76
80
|
"status.session_hint": "Используйте /sessions для выбора или /new для создания",
|
|
77
81
|
"status.server_unavailable": "🔴 OpenCode Server недоступен\n\nИспользуйте /opencode_start для запуска сервера.",
|
|
82
|
+
"tts.enabled": "🔊 Аудиоответы включены глобально.",
|
|
83
|
+
"tts.not_configured": "⚠️ Аудиоответы недоступны. Сначала укажите `TTS_API_URL` и `TTS_API_KEY`.",
|
|
84
|
+
"tts.disabled": "🔇 Аудиоответы выключены глобально.",
|
|
85
|
+
"tts.failed": "⚠️ Не удалось создать аудиоответ.",
|
|
78
86
|
"projects.empty": "📭 Проектов нет.\n\nОткройте директорию в OpenCode и создайте хотя бы одну сессию, после этого она появится здесь.",
|
|
79
87
|
"projects.select": "Выберите проект:",
|
|
80
88
|
"projects.select_with_current": "Выберите проект:\n\nТекущий: 🏗 {project}",
|
|
@@ -127,11 +135,11 @@ export const ru = {
|
|
|
127
135
|
"opencode_stop.stop_error": "🔴 Не удалось остановить OpenCode Server\n\nОшибка: {error}",
|
|
128
136
|
"opencode_stop.success": "✅ OpenCode Server успешно остановлен",
|
|
129
137
|
"opencode_stop.error": "🔴 Произошла ошибка при остановке сервера.\n\nПроверьте логи приложения для подробностей.",
|
|
130
|
-
"agent.changed_callback": "
|
|
131
|
-
"agent.changed_message": "✅
|
|
132
|
-
"agent.change_error_callback": "Ошибка при смене
|
|
133
|
-
"agent.menu.current": "Текущий
|
|
134
|
-
"agent.menu.select": "Выберите
|
|
138
|
+
"agent.changed_callback": "Агент изменен: {name}",
|
|
139
|
+
"agent.changed_message": "✅ Агент изменен на: {name}",
|
|
140
|
+
"agent.change_error_callback": "Ошибка при смене агента",
|
|
141
|
+
"agent.menu.current": "Текущий агент: {name}\n\nВыберите агента:",
|
|
142
|
+
"agent.menu.select": "Выберите агента:",
|
|
135
143
|
"agent.menu.empty": "⚠️ Нет доступных агентов",
|
|
136
144
|
"agent.menu.error": "🔴 Не удалось получить список агентов",
|
|
137
145
|
"model.changed_callback": "Модель изменена: {name}",
|
|
@@ -205,7 +213,7 @@ export const ru = {
|
|
|
205
213
|
"question.summary.title": "✅ Опрос завершен!\n\n",
|
|
206
214
|
"question.summary.question": "Вопрос {index}:\n{question}\n\n",
|
|
207
215
|
"question.summary.answer": "Ответ:\n{answer}\n\n",
|
|
208
|
-
"keyboard.agent_mode": "{emoji} {name}
|
|
216
|
+
"keyboard.agent_mode": "{emoji} {name} Agent",
|
|
209
217
|
"keyboard.context": "📊 {used} / {limit} ({percent}%)",
|
|
210
218
|
"keyboard.context_empty": "📊 0",
|
|
211
219
|
"keyboard.variant": "💭 {name}",
|
|
@@ -337,4 +345,19 @@ export const ru = {
|
|
|
337
345
|
"stt.not_configured": "🎤 Распознавание голоса не настроено.\n\nУстановите STT_API_URL и STT_API_KEY в .env для включения.",
|
|
338
346
|
"stt.error": "🔴 Не удалось распознать аудио: {error}",
|
|
339
347
|
"stt.empty_result": "🎤 В аудиосообщении не обнаружена речь.",
|
|
348
|
+
"cmd.description.open": "Добавить проект через обзор папок",
|
|
349
|
+
"open.back": "⬆️ Наверх",
|
|
350
|
+
"open.roots": "📋 К списку корней",
|
|
351
|
+
"open.prev_page": "⬅️ Назад",
|
|
352
|
+
"open.next_page": "Далее ➡️",
|
|
353
|
+
"open.select_current": "✅ Выбрать эту папку",
|
|
354
|
+
"open.select_root": "📂 Выберите корневой каталог для просмотра:",
|
|
355
|
+
"open.access_denied": "⛔ Доступ запрещён: путь за пределами разрешённых каталогов",
|
|
356
|
+
"open.scan_error": "🔴 Не удалось открыть каталог: {error}",
|
|
357
|
+
"open.open_error": "🔴 Не удалось открыть обозреватель каталогов.",
|
|
358
|
+
"open.selected": "✅ Проект добавлен: {project}\n\n📋 Используйте /sessions или /new для начала работы.",
|
|
359
|
+
"open.select_error": "🔴 Не удалось добавить проект.",
|
|
360
|
+
"open.no_subfolders": "📭 Нет подпапок",
|
|
361
|
+
"open.subfolder_count": "{count} подпапка",
|
|
362
|
+
"open.subfolders_count": "{count} подпапок",
|
|
340
363
|
};
|
package/dist/i18n/zh.js
CHANGED
|
@@ -3,6 +3,7 @@ export const zh = {
|
|
|
3
3
|
"cmd.description.new": "创建新会话",
|
|
4
4
|
"cmd.description.stop": "停止当前操作",
|
|
5
5
|
"cmd.description.sessions": "列出会话",
|
|
6
|
+
"cmd.description.tts": "切换语音回复",
|
|
6
7
|
"cmd.description.projects": "列出项目",
|
|
7
8
|
"cmd.description.task": "创建定时任务",
|
|
8
9
|
"cmd.description.tasklist": "查看定时任务",
|
|
@@ -32,8 +33,8 @@ export const zh = {
|
|
|
32
33
|
"inline.cancelled_callback": "已取消",
|
|
33
34
|
"common.unknown": "未知",
|
|
34
35
|
"common.unknown_error": "未知错误",
|
|
35
|
-
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n
|
|
36
|
-
"help.keyboard_hint": "💡
|
|
36
|
+
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择 Agent、模型和变体。",
|
|
37
|
+
"help.keyboard_hint": "💡 Agent、模型、变体和上下文操作请使用底部键盘按钮。",
|
|
37
38
|
"help.text": "📖 **帮助**\n\n/status - 查看服务器状态\n/sessions - 会话列表\n/new - 创建新会话\n/help - 帮助",
|
|
38
39
|
"bot.thinking": "💭 思考中...",
|
|
39
40
|
"bot.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
@@ -65,8 +66,11 @@ export const zh = {
|
|
|
65
66
|
"status.line.managed_no": "由机器人启动:否",
|
|
66
67
|
"status.line.pid": "PID:{pid}",
|
|
67
68
|
"status.line.uptime_sec": "运行时间:{seconds} 秒",
|
|
68
|
-
"status.line.mode": "
|
|
69
|
+
"status.line.mode": "Agent:{mode}",
|
|
69
70
|
"status.line.model": "模型:{model}",
|
|
71
|
+
"status.line.tts": "TTS 回复:{tts}",
|
|
72
|
+
"status.tts.on": "开启",
|
|
73
|
+
"status.tts.off": "关闭",
|
|
70
74
|
"status.agent_not_set": "未设置",
|
|
71
75
|
"status.project_selected": "项目:{project}",
|
|
72
76
|
"status.project_not_selected": "项目:未选择",
|
|
@@ -75,6 +79,10 @@ export const zh = {
|
|
|
75
79
|
"status.session_not_selected": "当前会话:未选择",
|
|
76
80
|
"status.session_hint": "使用 /sessions 选择一个会话,或 /new 创建",
|
|
77
81
|
"status.server_unavailable": "🔴 OpenCode 服务器不可用\n\n使用 /opencode_start 启动服务器。",
|
|
82
|
+
"tts.enabled": "🔊 已全局启用语音回复。",
|
|
83
|
+
"tts.not_configured": "⚠️ 语音回复暂不可用。请先设置 `TTS_API_URL` 和 `TTS_API_KEY`。",
|
|
84
|
+
"tts.disabled": "🔇 已全局关闭语音回复。",
|
|
85
|
+
"tts.failed": "⚠️ 生成语音回复失败。",
|
|
78
86
|
"projects.empty": "📭 未找到项目。\n\n在 OpenCode 中打开一个目录并至少创建一个会话,然后它会出现在这里。",
|
|
79
87
|
"projects.select": "请选择一个项目:",
|
|
80
88
|
"projects.select_with_current": "请选择一个项目:\n\n当前:🏗 {project}",
|
|
@@ -127,11 +135,11 @@ export const zh = {
|
|
|
127
135
|
"opencode_stop.stop_error": "🔴 停止 OpenCode 服务器失败\n\n错误:{error}",
|
|
128
136
|
"opencode_stop.success": "✅ OpenCode 服务器已成功停止",
|
|
129
137
|
"opencode_stop.error": "🔴 停止服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
130
|
-
"agent.changed_callback": "
|
|
131
|
-
"agent.changed_message": "✅
|
|
132
|
-
"agent.change_error_callback": "
|
|
133
|
-
"agent.menu.current": "
|
|
134
|
-
"agent.menu.select": "
|
|
138
|
+
"agent.changed_callback": "Agent 已更改:{name}",
|
|
139
|
+
"agent.changed_message": "✅ Agent 已切换为:{name}",
|
|
140
|
+
"agent.change_error_callback": "切换 Agent 失败",
|
|
141
|
+
"agent.menu.current": "当前 Agent:{name}\n\n请选择 Agent:",
|
|
142
|
+
"agent.menu.select": "请选择 Agent:",
|
|
135
143
|
"agent.menu.empty": "⚠️ 没有可用的代理",
|
|
136
144
|
"agent.menu.error": "🔴 获取代理列表失败",
|
|
137
145
|
"model.changed_callback": "模型已更改:{name}",
|
|
@@ -205,7 +213,7 @@ export const zh = {
|
|
|
205
213
|
"question.summary.title": "✅ 投票已完成!\n\n",
|
|
206
214
|
"question.summary.question": "问题 {index}:\n{question}\n\n",
|
|
207
215
|
"question.summary.answer": "回答:\n{answer}\n\n",
|
|
208
|
-
"keyboard.agent_mode": "{emoji} {name}
|
|
216
|
+
"keyboard.agent_mode": "{emoji} {name} Agent",
|
|
209
217
|
"keyboard.context": "📊 {used} / {limit} ({percent}%)",
|
|
210
218
|
"keyboard.context_empty": "📊 0",
|
|
211
219
|
"keyboard.variant": "💭 {name}",
|
|
@@ -337,4 +345,19 @@ export const zh = {
|
|
|
337
345
|
"stt.not_configured": "🎤 语音识别尚未配置。\n\n在 .env 中设置 STT_API_URL 和 STT_API_KEY 以启用。",
|
|
338
346
|
"stt.error": "🔴 识别音频失败:{error}",
|
|
339
347
|
"stt.empty_result": "🎤 音频消息中未检测到语音。",
|
|
348
|
+
"cmd.description.open": "通过浏览目录添加项目",
|
|
349
|
+
"open.back": "⬆️ 上级",
|
|
350
|
+
"open.roots": "📋 返回根目录",
|
|
351
|
+
"open.prev_page": "⬅️ 上一页",
|
|
352
|
+
"open.next_page": "下一页 ➡️",
|
|
353
|
+
"open.select_current": "✅ 选择此文件夹",
|
|
354
|
+
"open.select_root": "📂 选择要浏览的根目录:",
|
|
355
|
+
"open.access_denied": "⛔ 访问被拒绝:路径超出允许的目录范围",
|
|
356
|
+
"open.scan_error": "🔴 无法浏览目录:{error}",
|
|
357
|
+
"open.open_error": "🔴 无法打开目录浏览器。",
|
|
358
|
+
"open.selected": "✅ 项目已添加:{project}\n\n📋 使用 /sessions 或 /new 开始工作。",
|
|
359
|
+
"open.select_error": "🔴 添加项目失败。",
|
|
360
|
+
"open.no_subfolders": "📭 无子文件夹",
|
|
361
|
+
"open.subfolder_count": "{count} 个子文件夹",
|
|
362
|
+
"open.subfolders_count": "{count} 个子文件夹",
|
|
340
363
|
};
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ async function main() {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
setRuntimeMode(modeResult.mode);
|
|
15
|
+
const { initializeLogger } = await import("./utils/logger.js");
|
|
16
|
+
await initializeLogger();
|
|
15
17
|
const { startBotApp } = await import("./app/start-bot-app.js");
|
|
16
18
|
await startBotApp();
|
|
17
19
|
}
|
package/dist/pinned/manager.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { logger } from "../utils/logger.js";
|
|
2
4
|
import { opencodeClient } from "../opencode/client.js";
|
|
3
5
|
import { getCurrentSession } from "../session/manager.js";
|
|
@@ -15,6 +17,7 @@ class PinnedMessageManager {
|
|
|
15
17
|
sessionId: null,
|
|
16
18
|
sessionTitle: t("pinned.default_session_title"),
|
|
17
19
|
projectName: "",
|
|
20
|
+
projectBranch: null,
|
|
18
21
|
tokensUsed: 0,
|
|
19
22
|
tokensLimit: 0,
|
|
20
23
|
lastUpdated: 0,
|
|
@@ -52,9 +55,7 @@ class PinnedMessageManager {
|
|
|
52
55
|
// Update state
|
|
53
56
|
this.state.sessionId = sessionId;
|
|
54
57
|
this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
|
|
55
|
-
|
|
56
|
-
this.state.projectName =
|
|
57
|
-
project?.name || this.extractProjectName(project?.worktree) || t("pinned.unknown");
|
|
58
|
+
await this.refreshProjectMetadata();
|
|
58
59
|
// Fetch context limit for current model
|
|
59
60
|
await this.fetchContextLimit();
|
|
60
61
|
// Trigger keyboard update callback with reset context (0 tokens)
|
|
@@ -171,6 +172,7 @@ class PinnedMessageManager {
|
|
|
171
172
|
* Used at thinking time to push accumulated silent updates to Telegram.
|
|
172
173
|
*/
|
|
173
174
|
async refresh() {
|
|
175
|
+
await this.refreshProjectMetadata();
|
|
174
176
|
await this.updatePinnedMessage(true);
|
|
175
177
|
}
|
|
176
178
|
/**
|
|
@@ -412,6 +414,61 @@ class PinnedMessageManager {
|
|
|
412
414
|
logger.debug("[PinnedManager] Could not refresh session title:", err);
|
|
413
415
|
}
|
|
414
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Refresh current project name and git branch.
|
|
419
|
+
*/
|
|
420
|
+
async refreshProjectMetadata() {
|
|
421
|
+
const project = getCurrentProject();
|
|
422
|
+
this.state.projectName =
|
|
423
|
+
project?.name || this.extractProjectName(project?.worktree) || t("pinned.unknown");
|
|
424
|
+
this.state.projectBranch = await this.getGitBranchName(project?.worktree);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Resolve current git branch for a project worktree.
|
|
428
|
+
*/
|
|
429
|
+
async getGitBranchName(worktree) {
|
|
430
|
+
if (!worktree) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const gitDir = await this.resolveGitDir(worktree);
|
|
435
|
+
if (!gitDir) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const headPath = path.join(gitDir, "HEAD");
|
|
439
|
+
const headContent = (await readFile(headPath, "utf-8")).trim();
|
|
440
|
+
const match = headContent.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
441
|
+
return match?.[1] || null;
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
logger.debug("[PinnedManager] Could not resolve git branch:", err);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Resolve git directory for a normal repository or linked worktree.
|
|
450
|
+
*/
|
|
451
|
+
async resolveGitDir(worktree) {
|
|
452
|
+
const gitPath = path.join(worktree, ".git");
|
|
453
|
+
try {
|
|
454
|
+
const gitStat = await stat(gitPath);
|
|
455
|
+
if (gitStat.isDirectory()) {
|
|
456
|
+
return gitPath;
|
|
457
|
+
}
|
|
458
|
+
if (!gitStat.isFile()) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const gitPointer = (await readFile(gitPath, "utf-8")).trim();
|
|
462
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
|
|
463
|
+
if (!match) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
return path.resolve(worktree, match[1].trim());
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
415
472
|
/**
|
|
416
473
|
* Extract project name from worktree path
|
|
417
474
|
*/
|
|
@@ -482,9 +539,12 @@ class PinnedMessageManager {
|
|
|
482
539
|
formatMessage() {
|
|
483
540
|
const currentModel = getStoredModel();
|
|
484
541
|
const modelName = formatModelDisplayName(currentModel.providerID, currentModel.modelID);
|
|
542
|
+
const projectDisplayName = this.state.projectBranch
|
|
543
|
+
? `${this.state.projectName}: ${this.state.projectBranch}`
|
|
544
|
+
: this.state.projectName;
|
|
485
545
|
const lines = [
|
|
486
546
|
`${this.state.sessionTitle}`,
|
|
487
|
-
t("pinned.line.project", { project:
|
|
547
|
+
t("pinned.line.project", { project: projectDisplayName }),
|
|
488
548
|
t("pinned.line.model", { model: modelName }),
|
|
489
549
|
formatContextLine(this.state.tokensUsed, this.state.tokensLimit),
|
|
490
550
|
];
|
|
@@ -650,6 +710,7 @@ class PinnedMessageManager {
|
|
|
650
710
|
this.state.sessionId = null;
|
|
651
711
|
this.state.tokensUsed = 0;
|
|
652
712
|
this.state.tokensLimit = 0;
|
|
713
|
+
this.state.projectBranch = null;
|
|
653
714
|
this.state.changedFiles = [];
|
|
654
715
|
this.lastRenderedMessageText = null;
|
|
655
716
|
this.pendingUpdate = false;
|
|
@@ -665,6 +726,7 @@ class PinnedMessageManager {
|
|
|
665
726
|
this.state.sessionId = null;
|
|
666
727
|
this.state.sessionTitle = t("pinned.default_session_title");
|
|
667
728
|
this.state.projectName = "";
|
|
729
|
+
this.state.projectBranch = null;
|
|
668
730
|
this.state.tokensUsed = 0;
|
|
669
731
|
this.state.tokensLimit = 0;
|
|
670
732
|
this.state.changedFiles = [];
|
package/dist/project/manager.js
CHANGED
|
@@ -80,7 +80,8 @@ export async function getProjectById(id) {
|
|
|
80
80
|
}
|
|
81
81
|
export async function getProjectByWorktree(worktree) {
|
|
82
82
|
const projects = await getProjects();
|
|
83
|
-
const
|
|
83
|
+
const key = worktreeKey(worktree);
|
|
84
|
+
const project = projects.find((p) => worktreeKey(p.worktree) === key);
|
|
84
85
|
if (!project) {
|
|
85
86
|
throw new Error(`Project with worktree ${worktree} not found`);
|
|
86
87
|
}
|
package/dist/settings/manager.js
CHANGED
|
@@ -63,6 +63,13 @@ export function clearSession() {
|
|
|
63
63
|
currentSettings.currentSession = undefined;
|
|
64
64
|
void writeSettingsFile(currentSettings);
|
|
65
65
|
}
|
|
66
|
+
export function isTtsEnabled() {
|
|
67
|
+
return currentSettings.ttsEnabled === true;
|
|
68
|
+
}
|
|
69
|
+
export function setTtsEnabled(enabled) {
|
|
70
|
+
currentSettings.ttsEnabled = enabled;
|
|
71
|
+
void writeSettingsFile(currentSettings);
|
|
72
|
+
}
|
|
66
73
|
export function getCurrentAgent() {
|
|
67
74
|
return currentSettings.currentAgent;
|
|
68
75
|
}
|
|
@@ -43,6 +43,7 @@ class SummaryAggregator {
|
|
|
43
43
|
onSessionCompactedCallback = null;
|
|
44
44
|
onSessionErrorCallback = null;
|
|
45
45
|
onSessionRetryCallback = null;
|
|
46
|
+
onSessionIdleCallback = null;
|
|
46
47
|
onPermissionCallback = null;
|
|
47
48
|
onSessionDiffCallback = null;
|
|
48
49
|
onFileChangeCallback = null;
|
|
@@ -105,6 +106,9 @@ class SummaryAggregator {
|
|
|
105
106
|
setOnSessionRetry(callback) {
|
|
106
107
|
this.onSessionRetryCallback = callback;
|
|
107
108
|
}
|
|
109
|
+
setOnSessionIdle(callback) {
|
|
110
|
+
this.onSessionIdleCallback = callback;
|
|
111
|
+
}
|
|
108
112
|
setOnPermission(callback) {
|
|
109
113
|
this.onPermissionCallback = callback;
|
|
110
114
|
}
|
|
@@ -643,7 +647,13 @@ class SummaryAggregator {
|
|
|
643
647
|
this.onCostCallback(assistantInfo.cost);
|
|
644
648
|
}
|
|
645
649
|
if (this.onCompleteCallback && finalText.length > 0) {
|
|
646
|
-
this.onCompleteCallback(this.currentSessionId, messageID, finalText
|
|
650
|
+
this.onCompleteCallback(this.currentSessionId, messageID, finalText, {
|
|
651
|
+
agent: assistantMessage.agent,
|
|
652
|
+
providerID: assistantMessage.providerID,
|
|
653
|
+
modelID: assistantMessage.modelID,
|
|
654
|
+
createdAt: time?.created,
|
|
655
|
+
completedAt: time?.completed,
|
|
656
|
+
});
|
|
647
657
|
}
|
|
648
658
|
this.textMessageStates.delete(messageID);
|
|
649
659
|
this.messages.delete(messageID);
|
|
@@ -1038,6 +1048,12 @@ class SummaryAggregator {
|
|
|
1038
1048
|
logger.info(`[Aggregator] Session became idle: ${sessionID}`);
|
|
1039
1049
|
// Stop typing indicator when session goes idle
|
|
1040
1050
|
this.stopTypingIndicator();
|
|
1051
|
+
if (this.onSessionIdleCallback) {
|
|
1052
|
+
const callback = this.onSessionIdleCallback;
|
|
1053
|
+
setImmediate(() => {
|
|
1054
|
+
callback(sessionID);
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1041
1057
|
}
|
|
1042
1058
|
handleSessionCompacted(event) {
|
|
1043
1059
|
const properties = event.properties;
|
|
@@ -4,8 +4,18 @@ import { config } from "../config.js";
|
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { getCurrentProject } from "../settings/manager.js";
|
|
7
|
+
import { normalizeMarkdownForTelegramRendering } from "../telegram/render/markdown-normalizer.js";
|
|
7
8
|
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
8
9
|
const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
|
|
10
|
+
function truncateWithEllipsis(text, maxLength) {
|
|
11
|
+
if (text.length <= maxLength) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
if (maxLength <= 3) {
|
|
15
|
+
return ".".repeat(Math.max(0, maxLength));
|
|
16
|
+
}
|
|
17
|
+
return `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
18
|
+
}
|
|
9
19
|
function endsWithOddTrailingBackslashes(text, start, end) {
|
|
10
20
|
let backslashCount = 0;
|
|
11
21
|
for (let index = end - 1; index >= start; index--) {
|
|
@@ -50,87 +60,6 @@ function splitText(text, maxLength, options) {
|
|
|
50
60
|
}
|
|
51
61
|
return parts;
|
|
52
62
|
}
|
|
53
|
-
function isCodeFenceLine(line) {
|
|
54
|
-
return line.trimStart().startsWith("```");
|
|
55
|
-
}
|
|
56
|
-
function isHorizontalRuleLine(line) {
|
|
57
|
-
const normalized = line.trim();
|
|
58
|
-
if (!normalized) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
return /^([-*_])(?:\s*\1){2,}$/.test(normalized);
|
|
62
|
-
}
|
|
63
|
-
function isHeadingLine(line) {
|
|
64
|
-
return /^\s{0,3}#{1,6}\s+\S/.test(line);
|
|
65
|
-
}
|
|
66
|
-
function normalizeHeadingLine(line) {
|
|
67
|
-
const match = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*$/);
|
|
68
|
-
if (!match) {
|
|
69
|
-
return line;
|
|
70
|
-
}
|
|
71
|
-
return `**${match[1]}**`;
|
|
72
|
-
}
|
|
73
|
-
function normalizeChecklistLine(line) {
|
|
74
|
-
const match = line.match(/^(\s*)(?:[-+*]|\d+\.)\s+\[( |x|X)\]\s+(.*)$/);
|
|
75
|
-
if (!match) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const marker = match[2].toLowerCase() === "x" ? "✅" : "🔲";
|
|
79
|
-
return `${match[1]}${marker} ${match[3]}`;
|
|
80
|
-
}
|
|
81
|
-
function preprocessMarkdownForTelegram(text) {
|
|
82
|
-
const lines = text.split("\n");
|
|
83
|
-
const output = [];
|
|
84
|
-
let inCodeFence = false;
|
|
85
|
-
let inQuote = false;
|
|
86
|
-
for (let index = 0; index < lines.length; index++) {
|
|
87
|
-
const line = lines[index];
|
|
88
|
-
if (isCodeFenceLine(line)) {
|
|
89
|
-
inCodeFence = !inCodeFence;
|
|
90
|
-
inQuote = false;
|
|
91
|
-
output.push(line);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (inCodeFence) {
|
|
95
|
-
output.push(line);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (!line.trim()) {
|
|
99
|
-
inQuote = false;
|
|
100
|
-
output.push(line);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (isHeadingLine(line)) {
|
|
104
|
-
output.push(normalizeHeadingLine(line));
|
|
105
|
-
inQuote = false;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (isHorizontalRuleLine(line)) {
|
|
109
|
-
output.push("──────────");
|
|
110
|
-
inQuote = false;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const trimmedLeft = line.trimStart();
|
|
114
|
-
if (trimmedLeft.startsWith(">")) {
|
|
115
|
-
inQuote = true;
|
|
116
|
-
const quoteContent = trimmedLeft.replace(/^>\s?/, "");
|
|
117
|
-
const normalizedChecklistInQuote = normalizeChecklistLine(quoteContent);
|
|
118
|
-
output.push(normalizedChecklistInQuote ? `> ${normalizedChecklistInQuote.trimStart()}` : trimmedLeft);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const normalizedChecklist = normalizeChecklistLine(line);
|
|
122
|
-
if (normalizedChecklist) {
|
|
123
|
-
output.push(inQuote ? `> ${normalizedChecklist.trimStart()}` : normalizedChecklist);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (inQuote) {
|
|
127
|
-
output.push(`> ${trimmedLeft}`);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
output.push(line);
|
|
131
|
-
}
|
|
132
|
-
return output.join("\n");
|
|
133
|
-
}
|
|
134
63
|
export function normalizePathForDisplay(filePath) {
|
|
135
64
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
136
65
|
const project = getCurrentProject();
|
|
@@ -155,18 +84,12 @@ export function normalizePathForDisplay(filePath) {
|
|
|
155
84
|
export function formatSummary(text) {
|
|
156
85
|
return formatSummaryWithMode(text, config.bot.messageFormatMode);
|
|
157
86
|
}
|
|
158
|
-
export function getAssistantParseMode() {
|
|
159
|
-
if (config.bot.messageFormatMode === "markdown") {
|
|
160
|
-
return "MarkdownV2";
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
87
|
export function escapePlainTextForTelegramMarkdownV2(text) {
|
|
165
88
|
return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
|
|
166
89
|
}
|
|
167
90
|
function formatMarkdownForTelegram(text) {
|
|
168
91
|
try {
|
|
169
|
-
const preprocessed =
|
|
92
|
+
const preprocessed = normalizeMarkdownForTelegramRendering(text);
|
|
170
93
|
return escapeMarkdownV2PipesOutsideCode(convert(preprocessed, "keep"));
|
|
171
94
|
}
|
|
172
95
|
catch (error) {
|
|
@@ -380,7 +303,7 @@ export function formatToolInfo(toolInfo) {
|
|
|
380
303
|
description = `${input.description}\n`;
|
|
381
304
|
}
|
|
382
305
|
if (tool === "bash" && input && typeof input.command === "string") {
|
|
383
|
-
details = input.command;
|
|
306
|
+
details = truncateWithEllipsis(input.command, config.bot.bashToolDisplayMaxLength);
|
|
384
307
|
}
|
|
385
308
|
if (tool === "apply_patch") {
|
|
386
309
|
const filediff = toolInfo.metadata && "filediff" in toolInfo.metadata
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger.js";
|
|
2
|
+
import * as blockRenderer from "./block-renderer.js";
|
|
3
|
+
const BLOCK_RENDER_ORDER = ["full", "simplified", "line-by-line", "plain"];
|
|
4
|
+
export function renderTelegramBlockWithFallback(block) {
|
|
5
|
+
let lastError;
|
|
6
|
+
const failures = [];
|
|
7
|
+
for (const mode of BLOCK_RENDER_ORDER) {
|
|
8
|
+
try {
|
|
9
|
+
const renderedBlock = blockRenderer.renderTelegramBlock(block, mode);
|
|
10
|
+
if (failures.length > 0) {
|
|
11
|
+
logger.debug("[TelegramRender] Block fallback applied", {
|
|
12
|
+
blockType: block.type,
|
|
13
|
+
selectedMode: renderedBlock.mode,
|
|
14
|
+
failures,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return renderedBlock;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
lastError = error;
|
|
21
|
+
failures.push({
|
|
22
|
+
mode,
|
|
23
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to render Telegram block");
|
|
28
|
+
}
|