@grinev/opencode-telegram-bot 0.21.1 → 0.22.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 +10 -15
- package/README.md +33 -13
- package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
- package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
- package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
- package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +20 -132
- package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
- package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
- package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
- package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
- package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
- package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
- package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
- package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
- package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
- package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
- package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +177 -13
- package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
- package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
- package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
- package/dist/app/services/command-catalog-service.js +21 -0
- package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
- package/dist/app/services/file-browser-service.js +265 -0
- package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
- package/dist/app/services/mcp-catalog-service.js +79 -0
- package/dist/app/services/message-history-service.js +81 -0
- package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
- package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
- package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
- package/dist/{project/manager.js → app/services/project-service.js} +3 -3
- package/dist/app/services/project-switch-service.js +39 -0
- package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
- package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +27 -12
- package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
- package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
- package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
- package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
- package/dist/{session/manager.js → app/services/session-service.js} +1 -1
- package/dist/app/services/skills-catalog-service.js +21 -0
- package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
- package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
- package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
- package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
- package/dist/{settings/manager.js → app/stores/settings-store.js} +49 -7
- package/dist/app/types/worktree.js +1 -0
- package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
- package/dist/bot/callbacks/callback-router.js +113 -0
- package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
- package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
- package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
- package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
- package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
- package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
- package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
- package/dist/bot/callbacks/permission-callback-handler.js +114 -0
- package/dist/bot/callbacks/project-callback-handler.js +79 -0
- package/dist/bot/callbacks/question-callback-handler.js +159 -0
- package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
- package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
- package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
- package/dist/bot/callbacks/settings-callback-handler.js +105 -0
- package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
- package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
- package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
- package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
- package/dist/bot/commands/command-catalog-command.js +42 -0
- package/dist/bot/commands/definitions.js +1 -1
- package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
- package/dist/bot/commands/ls-command.js +48 -0
- package/dist/bot/commands/mcp-catalog-command.js +39 -0
- package/dist/bot/commands/messages-command.js +59 -0
- package/dist/bot/commands/{new.js → new-command.js} +12 -11
- package/dist/bot/commands/open-command.js +45 -0
- package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
- package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
- package/dist/bot/commands/projects-command.js +32 -0
- package/dist/bot/commands/rename-command.js +33 -0
- package/dist/bot/commands/sessions-command.js +42 -0
- package/dist/bot/commands/settings-command.js +10 -0
- package/dist/bot/commands/skills-catalog-command.js +42 -0
- package/dist/bot/commands/{start.js → start-command.js} +12 -12
- package/dist/bot/commands/{status.js → status-command.js} +15 -10
- package/dist/bot/commands/{task.js → task-command.js} +9 -81
- package/dist/bot/commands/tasklist-command.js +40 -0
- package/dist/bot/commands/worktree-command.js +47 -0
- package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
- package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
- package/dist/bot/handlers/photo-handler.js +44 -0
- package/dist/bot/handlers/prompt.js +17 -17
- package/dist/bot/handlers/text-message-handler.js +61 -0
- package/dist/bot/handlers/tts-response-handler.js +29 -0
- package/dist/bot/handlers/{voice.js → voice-handler.js} +6 -3
- package/dist/bot/index.js +27 -1179
- package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
- package/dist/bot/keyboards/keyboard-types.js +1 -0
- package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
- package/dist/bot/menus/agent-selection-menu.js +54 -0
- package/dist/bot/menus/command-catalog-menu.js +101 -0
- package/dist/bot/menus/context-control-menu.js +33 -0
- package/dist/bot/menus/file-browser-menu.js +265 -0
- package/dist/bot/{handlers → menus}/inline-menu.js +7 -34
- package/dist/bot/menus/mcp-catalog-menu.js +97 -0
- package/dist/bot/menus/message-history-menu.js +107 -0
- package/dist/bot/menus/model-selection-menu.js +66 -0
- package/dist/bot/menus/permission-menu.js +116 -0
- package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
- package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
- package/dist/bot/menus/rename-menu.js +6 -0
- package/dist/bot/menus/scheduled-task-menu.js +42 -0
- package/dist/bot/menus/session-selection-menu.js +115 -0
- package/dist/bot/menus/settings-menu.js +52 -0
- package/dist/bot/menus/skills-catalog-menu.js +99 -0
- package/dist/bot/menus/variant-selection-menu.js +67 -0
- package/dist/bot/menus/worktree-selection-menu.js +76 -0
- package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
- package/dist/bot/messages/busy-blocked-renderer.js +11 -0
- package/dist/bot/messages/external-user-input-notification.js +22 -0
- package/dist/bot/messages/scheduled-task-delivery.js +63 -0
- package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
- package/dist/bot/messages/summary-message-formatter.js +130 -0
- package/dist/bot/{utils → messages}/thinking-message.js +1 -4
- package/dist/bot/messages/thinking-rendering.js +66 -0
- package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
- package/dist/bot/middleware/interaction-guard.js +2 -2
- package/dist/bot/middleware/unknown-command.js +1 -1
- package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
- package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
- package/dist/bot/pinned/pinned-message-types.js +1 -0
- package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
- package/dist/bot/render/types.js +1 -0
- package/dist/bot/routers/command-router.js +73 -0
- package/dist/bot/routers/message-router.js +150 -0
- package/dist/bot/services/attach-presentation.js +40 -0
- package/dist/bot/services/event-subscription-service.js +1116 -0
- package/dist/bot/services/project-switch-presentation.js +26 -0
- package/dist/bot/streaming/compact-progress-streamer.js +162 -0
- package/dist/bot/{utils → streaming}/finalize-assistant-response.js +9 -3
- package/dist/bot/streaming/response-streamer.js +16 -1
- package/dist/cli.js +4 -4
- package/dist/config.js +9 -18
- package/dist/i18n/ar.js +31 -6
- package/dist/i18n/de.js +32 -7
- package/dist/i18n/en.js +32 -7
- package/dist/i18n/es.js +32 -7
- package/dist/i18n/fr.js +32 -7
- package/dist/i18n/ru.js +32 -7
- package/dist/i18n/zh.js +32 -7
- package/dist/index.js +1 -1
- package/dist/opencode/ready-refresh.js +2 -2
- package/dist/{service → runtime/service}/manager.js +2 -2
- package/dist/runtime/service/types.js +1 -0
- package/package.json +1 -1
- package/dist/bot/commands/ls.js +0 -445
- package/dist/bot/commands/open.js +0 -319
- package/dist/bot/commands/tts.js +0 -13
- package/dist/bot/commands/worktree.js +0 -201
- package/dist/bot/handlers/permission.js +0 -235
- package/dist/bot/utils/browser-roots.js +0 -140
- package/dist/bot/utils/file-tree.js +0 -92
- package/dist/bot/utils/send-tts-response.js +0 -37
- package/dist/bot/utils/switch-project.js +0 -45
- package/dist/bot/utils/telegram-file-url.js +0 -11
- package/dist/interaction/busy.js +0 -8
- package/dist/interaction/cleanup.js +0 -32
- /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
- /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
- /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
- /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
- /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
- /package/dist/{agent/types.js → app/types/agent.js} +0 -0
- /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
- /package/dist/{model/types.js → app/types/model.js} +0 -0
- /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
- /package/dist/{permission/types.js → app/types/project.js} +0 -0
- /package/dist/{pinned/types.js → app/types/question.js} +0 -0
- /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
- /package/dist/{question/types.js → app/types/session.js} +0 -0
- /package/dist/{service/types.js → app/types/settings.js} +0 -0
- /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
- /package/dist/{variant/types.js → app/types/variant.js} +0 -0
- /package/dist/bot/commands/{help.js → help-command.js} +0 -0
- /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
- /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
- /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
- /package/dist/{telegram → bot}/render/block-parser.js +0 -0
- /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
- /package/dist/{telegram → bot}/render/chunker.js +0 -0
- /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
- /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
- /package/dist/{telegram → bot}/render/pipeline.js +0 -0
- /package/dist/{telegram → bot}/render/validator.js +0 -0
- /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
- /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { appendInlineMenuCancelButton, ensureActiveInlineMenu } from "../handlers/inline-menu.js";
|
|
4
|
-
import { interactionManager } from "../../interaction/manager.js";
|
|
5
|
-
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
6
|
-
import { pathToDisplayPath, scanDirectory, buildEntryLabel, buildTreeHeader, isScanError, MAX_ENTRIES_PER_PAGE, } from "../utils/file-tree.js";
|
|
7
|
-
import { getBrowserRoots, isWithinAllowedRoot, isAllowedRoot } from "../utils/browser-roots.js";
|
|
8
|
-
import { upsertSessionDirectory } from "../../session/cache-manager.js";
|
|
9
|
-
import { getProjectByWorktree } from "../../project/manager.js";
|
|
10
|
-
import { switchToProject } from "../utils/switch-project.js";
|
|
11
|
-
import { logger } from "../../utils/logger.js";
|
|
12
|
-
import { t } from "../../i18n/index.js";
|
|
13
|
-
const CALLBACK_PREFIX = "open:";
|
|
14
|
-
const CALLBACK_NAV_PREFIX = "open:nav:";
|
|
15
|
-
const CALLBACK_SELECT_PREFIX = "open:sel:";
|
|
16
|
-
const CALLBACK_PAGE_PREFIX = "open:pg:";
|
|
17
|
-
const CALLBACK_ROOTS = "open:roots";
|
|
18
|
-
const MAX_BUTTON_LABEL_LENGTH = 64;
|
|
19
|
-
/**
|
|
20
|
-
* Separator used inside pagination callback data between the encoded path
|
|
21
|
-
* reference and the page number. We avoid `:` because it appears in Windows
|
|
22
|
-
* drive letters (e.g. `C:\`) and is already used as a prefix delimiter.
|
|
23
|
-
*/
|
|
24
|
-
const PAGE_SEPARATOR = "|";
|
|
25
|
-
function truncateLabel(label, maxLen = MAX_BUTTON_LABEL_LENGTH) {
|
|
26
|
-
if (label.length <= maxLen) {
|
|
27
|
-
return label;
|
|
28
|
-
}
|
|
29
|
-
return label.slice(0, Math.max(0, maxLen - 3)) + "...";
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Encode a path into callback data. Telegram limits callback_data to 64 bytes.
|
|
33
|
-
* Long absolute paths can exceed this, so we encode them as a compact index
|
|
34
|
-
* when necessary. The index is stored in a module-level map that lives for the
|
|
35
|
-
* duration of the current inline menu interaction.
|
|
36
|
-
*/
|
|
37
|
-
const pathIndex = new Map();
|
|
38
|
-
let pathCounter = 0;
|
|
39
|
-
/** Clear the path index. Exported so it can be called on menu cancel/cleanup. */
|
|
40
|
-
export function clearOpenPathIndex() {
|
|
41
|
-
pathIndex.clear();
|
|
42
|
-
pathCounter = 0;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* @param prefix Callback-data prefix that precedes the path.
|
|
46
|
-
* @param fullPath Absolute path to encode.
|
|
47
|
-
* @param reserveBytes Extra bytes to reserve for a suffix that will be
|
|
48
|
-
* appended *after* the returned value (e.g. the page separator + digits in
|
|
49
|
-
* pagination callbacks). The total callback_data must stay ≤ 64 bytes.
|
|
50
|
-
*/
|
|
51
|
-
function encodePathForCallback(prefix, fullPath, reserveBytes = 0) {
|
|
52
|
-
const naive = `${prefix}${fullPath}`;
|
|
53
|
-
if (Buffer.byteLength(naive, "utf-8") + reserveBytes <= 64) {
|
|
54
|
-
return naive;
|
|
55
|
-
}
|
|
56
|
-
// Use a short numeric key instead
|
|
57
|
-
const key = `#${pathCounter++}`;
|
|
58
|
-
pathIndex.set(key, fullPath);
|
|
59
|
-
return `${prefix}${key}`;
|
|
60
|
-
}
|
|
61
|
-
function decodePathFromCallback(prefix, data) {
|
|
62
|
-
if (!data.startsWith(prefix)) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
const raw = data.slice(prefix.length);
|
|
66
|
-
if (raw.startsWith("#")) {
|
|
67
|
-
return pathIndex.get(raw) ?? null;
|
|
68
|
-
}
|
|
69
|
-
return raw;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Encode a pagination callback. The path part goes through the same 64-byte
|
|
73
|
-
* safe encoding used by nav/select callbacks, followed by a separator and
|
|
74
|
-
* the page number.
|
|
75
|
-
*
|
|
76
|
-
* We reserve bytes for the page suffix so the 64-byte check inside
|
|
77
|
-
* `encodePathForCallback` accounts for the complete final callback length.
|
|
78
|
-
*/
|
|
79
|
-
function encodePaginationCallback(currentPath, page) {
|
|
80
|
-
const pageSuffix = `${PAGE_SEPARATOR}${page}`;
|
|
81
|
-
const reserveBytes = Buffer.byteLength(pageSuffix, "utf-8");
|
|
82
|
-
const pathRef = encodePathForCallback(CALLBACK_PAGE_PREFIX, currentPath, reserveBytes);
|
|
83
|
-
return `${pathRef}${pageSuffix}`;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Decode a pagination callback into { path, page } or null on failure.
|
|
87
|
-
*/
|
|
88
|
-
function decodePaginationCallback(data) {
|
|
89
|
-
if (!data.startsWith(CALLBACK_PAGE_PREFIX)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const payload = data.slice(CALLBACK_PAGE_PREFIX.length);
|
|
93
|
-
const sepIndex = payload.lastIndexOf(PAGE_SEPARATOR);
|
|
94
|
-
if (sepIndex < 0) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const pathRef = payload.slice(0, sepIndex);
|
|
98
|
-
const pageNum = Number.parseInt(payload.slice(sepIndex + 1), 10);
|
|
99
|
-
if (Number.isNaN(pageNum)) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
// Resolve indexed path references
|
|
103
|
-
const resolvedPath = pathRef.startsWith("#") ? (pathIndex.get(pathRef) ?? null) : pathRef;
|
|
104
|
-
if (resolvedPath === null) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
return { path: resolvedPath, page: pageNum };
|
|
108
|
-
}
|
|
109
|
-
function buildBrowseKeyboard(entries, currentPath, hasParent, page, totalCount) {
|
|
110
|
-
const keyboard = new InlineKeyboard();
|
|
111
|
-
const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
|
|
112
|
-
// Directory entries
|
|
113
|
-
for (const entry of entries) {
|
|
114
|
-
const label = truncateLabel(buildEntryLabel(entry));
|
|
115
|
-
keyboard.text(label, encodePathForCallback(CALLBACK_NAV_PREFIX, entry.fullPath)).row();
|
|
116
|
-
}
|
|
117
|
-
// Navigation: Up + Back to roots
|
|
118
|
-
// Suppress "Up" when at an allowed root (don't let user navigate above it)
|
|
119
|
-
const atRoot = isAllowedRoot(currentPath);
|
|
120
|
-
const showUp = hasParent && !atRoot;
|
|
121
|
-
const roots = getBrowserRoots();
|
|
122
|
-
const showRoots = roots.length > 1;
|
|
123
|
-
if (showUp || showRoots) {
|
|
124
|
-
if (showUp) {
|
|
125
|
-
const parentPath = path.dirname(currentPath);
|
|
126
|
-
keyboard.text(t("open.back"), encodePathForCallback(CALLBACK_NAV_PREFIX, parentPath));
|
|
127
|
-
}
|
|
128
|
-
if (showRoots) {
|
|
129
|
-
keyboard.text(t("open.roots"), CALLBACK_ROOTS);
|
|
130
|
-
}
|
|
131
|
-
keyboard.row();
|
|
132
|
-
}
|
|
133
|
-
// Pagination
|
|
134
|
-
if (totalPages > 1) {
|
|
135
|
-
if (page > 0) {
|
|
136
|
-
keyboard.text(t("open.prev_page"), encodePaginationCallback(currentPath, page - 1));
|
|
137
|
-
}
|
|
138
|
-
if (page < totalPages - 1) {
|
|
139
|
-
keyboard.text(t("open.next_page"), encodePaginationCallback(currentPath, page + 1));
|
|
140
|
-
}
|
|
141
|
-
keyboard.row();
|
|
142
|
-
}
|
|
143
|
-
// Select current folder
|
|
144
|
-
keyboard
|
|
145
|
-
.text(t("open.select_current"), encodePathForCallback(CALLBACK_SELECT_PREFIX, currentPath))
|
|
146
|
-
.row();
|
|
147
|
-
// Cancel
|
|
148
|
-
appendInlineMenuCancelButton(keyboard, "open");
|
|
149
|
-
return keyboard;
|
|
150
|
-
}
|
|
151
|
-
async function renderBrowseView(dirPath, page = 0) {
|
|
152
|
-
const result = await scanDirectory(dirPath, page);
|
|
153
|
-
if (isScanError(result)) {
|
|
154
|
-
return { error: result.error };
|
|
155
|
-
}
|
|
156
|
-
const { entries, totalCount, page: clampedPage, currentPath, displayPath, hasParent } = result;
|
|
157
|
-
const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
|
|
158
|
-
const header = buildTreeHeader(displayPath, totalCount, clampedPage, totalPages);
|
|
159
|
-
const keyboard = buildBrowseKeyboard(entries, currentPath, hasParent, clampedPage, totalCount);
|
|
160
|
-
return { text: header, keyboard };
|
|
161
|
-
}
|
|
162
|
-
function buildRootsKeyboard() {
|
|
163
|
-
const keyboard = new InlineKeyboard();
|
|
164
|
-
const roots = getBrowserRoots();
|
|
165
|
-
for (const root of roots) {
|
|
166
|
-
const label = truncateLabel(`📂 ${pathToDisplayPath(root)}`);
|
|
167
|
-
keyboard.text(label, encodePathForCallback(CALLBACK_NAV_PREFIX, root)).row();
|
|
168
|
-
}
|
|
169
|
-
appendInlineMenuCancelButton(keyboard, "open");
|
|
170
|
-
return keyboard;
|
|
171
|
-
}
|
|
172
|
-
export async function openCommand(ctx) {
|
|
173
|
-
try {
|
|
174
|
-
if (isForegroundBusy()) {
|
|
175
|
-
await replyBusyBlocked(ctx);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
// Reset path index for new interaction
|
|
179
|
-
clearOpenPathIndex();
|
|
180
|
-
const roots = getBrowserRoots();
|
|
181
|
-
let text;
|
|
182
|
-
let keyboard;
|
|
183
|
-
if (roots.length === 1) {
|
|
184
|
-
// Single root — navigate directly into it
|
|
185
|
-
const view = await renderBrowseView(roots[0]);
|
|
186
|
-
if ("error" in view) {
|
|
187
|
-
await ctx.reply(t("open.scan_error", { error: view.error }));
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
text = view.text;
|
|
191
|
-
keyboard = view.keyboard;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
// Multiple roots — show root selection
|
|
195
|
-
text = t("open.select_root");
|
|
196
|
-
keyboard = buildRootsKeyboard();
|
|
197
|
-
}
|
|
198
|
-
const message = await ctx.reply(text, { reply_markup: keyboard });
|
|
199
|
-
interactionManager.start({
|
|
200
|
-
kind: "inline",
|
|
201
|
-
expectedInput: "callback",
|
|
202
|
-
metadata: {
|
|
203
|
-
menuKind: "open",
|
|
204
|
-
messageId: message.message_id,
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
logger.error("[Bot] Error opening directory browser:", error);
|
|
210
|
-
await ctx.reply(t("open.open_error"));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
export async function handleOpenCallback(ctx, deps = {}) {
|
|
214
|
-
const data = ctx.callbackQuery?.data;
|
|
215
|
-
if (!data || !data.startsWith(CALLBACK_PREFIX)) {
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
if (isForegroundBusy()) {
|
|
219
|
-
await replyBusyBlocked(ctx);
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
const isActiveMenu = await ensureActiveInlineMenu(ctx, "open");
|
|
223
|
-
if (!isActiveMenu) {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
// Back to root selection (multi-root mode)
|
|
228
|
-
if (data === CALLBACK_ROOTS) {
|
|
229
|
-
await showRoots(ctx);
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
// Navigate into a directory (including "up")
|
|
233
|
-
const navPath = decodePathFromCallback(CALLBACK_NAV_PREFIX, data);
|
|
234
|
-
if (navPath !== null) {
|
|
235
|
-
if (!isWithinAllowedRoot(navPath)) {
|
|
236
|
-
await ctx.answerCallbackQuery({ text: t("open.access_denied") });
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
await navigateTo(ctx, navPath);
|
|
240
|
-
return true;
|
|
241
|
-
}
|
|
242
|
-
// Pagination
|
|
243
|
-
const pageInfo = decodePaginationCallback(data);
|
|
244
|
-
if (pageInfo !== null) {
|
|
245
|
-
if (!isWithinAllowedRoot(pageInfo.path)) {
|
|
246
|
-
await ctx.answerCallbackQuery({ text: t("open.access_denied") });
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
await navigateTo(ctx, pageInfo.path, pageInfo.page);
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
// Select directory as project
|
|
253
|
-
const selectPath = decodePathFromCallback(CALLBACK_SELECT_PREFIX, data);
|
|
254
|
-
if (selectPath !== null) {
|
|
255
|
-
if (!isWithinAllowedRoot(selectPath)) {
|
|
256
|
-
await ctx.answerCallbackQuery({ text: t("open.access_denied") });
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
await selectDirectory(ctx, selectPath, deps);
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
logger.error("[Bot] Error handling open callback:", error);
|
|
266
|
-
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
async function showRoots(ctx) {
|
|
271
|
-
const text = t("open.select_root");
|
|
272
|
-
const keyboard = buildRootsKeyboard();
|
|
273
|
-
await ctx.answerCallbackQuery();
|
|
274
|
-
await ctx.editMessageText(text, { reply_markup: keyboard });
|
|
275
|
-
}
|
|
276
|
-
async function navigateTo(ctx, dirPath, page = 0) {
|
|
277
|
-
const view = await renderBrowseView(dirPath, page);
|
|
278
|
-
if ("error" in view) {
|
|
279
|
-
await ctx.answerCallbackQuery({ text: view.error });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
await ctx.answerCallbackQuery();
|
|
283
|
-
await ctx.editMessageText(view.text, {
|
|
284
|
-
reply_markup: view.keyboard,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
async function selectDirectory(ctx, directory, deps = {}) {
|
|
288
|
-
const displayPath = pathToDisplayPath(directory);
|
|
289
|
-
try {
|
|
290
|
-
logger.info(`[Bot] Adding project directory: ${directory}`);
|
|
291
|
-
// Register directory in the session cache first — getProjectByWorktree
|
|
292
|
-
// needs the entry to exist so it can resolve the project. If the
|
|
293
|
-
// subsequent switch fails, the entry stays in the cache, which is
|
|
294
|
-
// acceptable: it's a real directory the user explicitly selected and
|
|
295
|
-
// will simply appear in /projects for retry.
|
|
296
|
-
await upsertSessionDirectory(directory, Date.now());
|
|
297
|
-
const projectInfo = await getProjectByWorktree(directory);
|
|
298
|
-
const selectedProjectInfo = { ...projectInfo, name: displayPath };
|
|
299
|
-
const replyKeyboard = deps.ensureEventSubscription
|
|
300
|
-
? await switchToProject(ctx, selectedProjectInfo, "open_project_selected", {
|
|
301
|
-
ensureEventSubscription: deps.ensureEventSubscription,
|
|
302
|
-
})
|
|
303
|
-
: await switchToProject(ctx, selectedProjectInfo, "open_project_selected");
|
|
304
|
-
await ctx.answerCallbackQuery();
|
|
305
|
-
await ctx.reply(t("open.selected", { project: displayPath }), {
|
|
306
|
-
reply_markup: replyKeyboard,
|
|
307
|
-
});
|
|
308
|
-
// Clean up the inline menu message
|
|
309
|
-
await ctx.deleteMessage();
|
|
310
|
-
// Clear path index after selection
|
|
311
|
-
clearOpenPathIndex();
|
|
312
|
-
logger.info(`[Bot] Project added and selected: ${displayPath}`);
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
logger.error("[Bot] Error selecting directory:", error);
|
|
316
|
-
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
317
|
-
await ctx.reply(t("open.select_error"));
|
|
318
|
-
}
|
|
319
|
-
}
|
package/dist/bot/commands/tts.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { isTtsConfigured } from "../../tts/client.js";
|
|
2
|
-
import { isTtsEnabled, setTtsEnabled } from "../../settings/manager.js";
|
|
3
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
-
export async function ttsCommand(ctx) {
|
|
5
|
-
const enabled = !isTtsEnabled();
|
|
6
|
-
if (enabled && !isTtsConfigured()) {
|
|
7
|
-
await ctx.reply(t("tts.not_configured"));
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
setTtsEnabled(enabled);
|
|
11
|
-
const message = enabled ? t("tts.enabled") : t("tts.disabled");
|
|
12
|
-
await ctx.reply(message);
|
|
13
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { config } from "../../config.js";
|
|
3
|
-
import { getGitWorktreeContext } from "../../git/worktree.js";
|
|
4
|
-
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
5
|
-
import { getProjectByWorktree } from "../../project/manager.js";
|
|
6
|
-
import { upsertSessionDirectory } from "../../session/cache-manager.js";
|
|
7
|
-
import { getCurrentProject } from "../../settings/manager.js";
|
|
8
|
-
import { logger } from "../../utils/logger.js";
|
|
9
|
-
import { t } from "../../i18n/index.js";
|
|
10
|
-
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
11
|
-
import { switchToProject } from "../utils/switch-project.js";
|
|
12
|
-
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
13
|
-
import { buildProjectButtonLabel, calculateProjectsPaginationRange } from "./projects.js";
|
|
14
|
-
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
15
|
-
const WORKTREE_CALLBACK_PREFIX = "worktree:";
|
|
16
|
-
const WORKTREE_PAGE_CALLBACK_PREFIX = "worktree:page:";
|
|
17
|
-
function formatWorktreeButtonLabel(label, isActive) {
|
|
18
|
-
const prefix = isActive ? "✅ " : "";
|
|
19
|
-
const availableLength = MAX_INLINE_BUTTON_LABEL_LENGTH - prefix.length;
|
|
20
|
-
if (label.length <= availableLength) {
|
|
21
|
-
return `${prefix}${label}`;
|
|
22
|
-
}
|
|
23
|
-
return `${prefix}${label.slice(0, Math.max(0, availableLength - 3))}...`;
|
|
24
|
-
}
|
|
25
|
-
function buildWorktreeButtonLabel(index, entry) {
|
|
26
|
-
return buildProjectButtonLabel(index, entry.path);
|
|
27
|
-
}
|
|
28
|
-
function parseWorktreePageCallback(data) {
|
|
29
|
-
if (!data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const rawPage = data.slice(WORKTREE_PAGE_CALLBACK_PREFIX.length);
|
|
33
|
-
if (!/^\d+$/.test(rawPage)) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return Number.parseInt(rawPage, 10);
|
|
37
|
-
}
|
|
38
|
-
function parseWorktreeIndexCallback(data) {
|
|
39
|
-
if (!data.startsWith(WORKTREE_CALLBACK_PREFIX) ||
|
|
40
|
-
data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const rawIndex = data.slice(WORKTREE_CALLBACK_PREFIX.length);
|
|
44
|
-
if (!/^\d+$/.test(rawIndex)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
return Number.parseInt(rawIndex, 10);
|
|
48
|
-
}
|
|
49
|
-
function buildWorktreeMenuText(page, totalPages) {
|
|
50
|
-
const baseText = t("worktree.select_with_current");
|
|
51
|
-
if (totalPages <= 1) {
|
|
52
|
-
return baseText;
|
|
53
|
-
}
|
|
54
|
-
return `${baseText}\n\n${t("projects.page_indicator", {
|
|
55
|
-
current: String(page + 1),
|
|
56
|
-
total: String(totalPages),
|
|
57
|
-
})}`;
|
|
58
|
-
}
|
|
59
|
-
function buildWorktreeKeyboard(worktrees, page) {
|
|
60
|
-
const keyboard = new InlineKeyboard();
|
|
61
|
-
const pageSize = config.bot.projectsListLimit;
|
|
62
|
-
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(worktrees.length, page, pageSize);
|
|
63
|
-
worktrees.slice(startIndex, endIndex).forEach((entry, index) => {
|
|
64
|
-
const label = buildWorktreeButtonLabel(startIndex + index, entry);
|
|
65
|
-
keyboard
|
|
66
|
-
.text(formatWorktreeButtonLabel(label, entry.isCurrent), `${WORKTREE_CALLBACK_PREFIX}${startIndex + index}`)
|
|
67
|
-
.row();
|
|
68
|
-
});
|
|
69
|
-
if (totalPages > 1) {
|
|
70
|
-
if (normalizedPage > 0) {
|
|
71
|
-
keyboard.text(t("projects.prev_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage - 1}`);
|
|
72
|
-
}
|
|
73
|
-
if (normalizedPage < totalPages - 1) {
|
|
74
|
-
keyboard.text(t("projects.next_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage + 1}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return keyboard;
|
|
78
|
-
}
|
|
79
|
-
function buildWorktreeMenuView(worktrees, page) {
|
|
80
|
-
const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(worktrees.length, page, config.bot.projectsListLimit);
|
|
81
|
-
return {
|
|
82
|
-
text: buildWorktreeMenuText(normalizedPage, totalPages),
|
|
83
|
-
keyboard: buildWorktreeKeyboard(worktrees, normalizedPage),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
async function loadCurrentWorktreeContext() {
|
|
87
|
-
const currentProject = getCurrentProject();
|
|
88
|
-
if (!currentProject) {
|
|
89
|
-
return { currentProject: null, context: null };
|
|
90
|
-
}
|
|
91
|
-
const context = await getGitWorktreeContext(currentProject.worktree);
|
|
92
|
-
return { currentProject, context };
|
|
93
|
-
}
|
|
94
|
-
export async function worktreeCommand(ctx) {
|
|
95
|
-
try {
|
|
96
|
-
if (isForegroundBusy()) {
|
|
97
|
-
await replyBusyBlocked(ctx);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const { currentProject, context } = await loadCurrentWorktreeContext();
|
|
101
|
-
if (!currentProject) {
|
|
102
|
-
await ctx.reply(t("worktree.project_not_selected"));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (!context) {
|
|
106
|
-
await ctx.reply(t("worktree.not_git_repo"));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (context.worktrees.length === 0) {
|
|
110
|
-
await ctx.reply(t("worktree.empty"));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const { text, keyboard } = buildWorktreeMenuView(context.worktrees, 0);
|
|
114
|
-
await replyWithInlineMenu(ctx, {
|
|
115
|
-
menuKind: "worktree",
|
|
116
|
-
text,
|
|
117
|
-
keyboard,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
logger.error("[Bot] Error loading worktrees:", error);
|
|
122
|
-
await ctx.reply(t("worktree.fetch_error"));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export async function handleWorktreeCallback(ctx, deps = {}) {
|
|
126
|
-
const callbackQuery = ctx.callbackQuery;
|
|
127
|
-
if (!callbackQuery?.data || !callbackQuery.data.startsWith(WORKTREE_CALLBACK_PREFIX)) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
if (isForegroundBusy()) {
|
|
131
|
-
await replyBusyBlocked(ctx);
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
const page = parseWorktreePageCallback(callbackQuery.data);
|
|
135
|
-
const index = parseWorktreeIndexCallback(callbackQuery.data);
|
|
136
|
-
const isActiveMenu = await ensureActiveInlineMenu(ctx, "worktree");
|
|
137
|
-
if (!isActiveMenu) {
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const { currentProject, context } = await loadCurrentWorktreeContext();
|
|
142
|
-
if (!currentProject) {
|
|
143
|
-
clearAllInteractionState("worktree_project_missing");
|
|
144
|
-
await ctx.answerCallbackQuery();
|
|
145
|
-
await ctx.reply(t("worktree.project_not_selected"));
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
if (!context) {
|
|
149
|
-
clearAllInteractionState("worktree_git_context_missing");
|
|
150
|
-
await ctx.answerCallbackQuery({ text: t("worktree.not_git_repo_callback") });
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
if (page !== null) {
|
|
154
|
-
if (context.worktrees.length === 0) {
|
|
155
|
-
await ctx.answerCallbackQuery({ text: t("worktree.page_empty_callback") });
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
const { text, keyboard } = buildWorktreeMenuView(context.worktrees, page);
|
|
159
|
-
await ctx.answerCallbackQuery();
|
|
160
|
-
await ctx.editMessageText(text, {
|
|
161
|
-
reply_markup: appendInlineMenuCancelButton(keyboard, "worktree"),
|
|
162
|
-
});
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
if (index === null) {
|
|
166
|
-
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const selectedWorktree = context.worktrees[index];
|
|
170
|
-
if (!selectedWorktree) {
|
|
171
|
-
await ctx.answerCallbackQuery({ text: t("worktree.selection_missing_callback") });
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
if (selectedWorktree.isCurrent) {
|
|
175
|
-
await ctx.answerCallbackQuery({ text: t("worktree.already_selected_callback") });
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
logger.info(`[Bot] Worktree selected: ${selectedWorktree.path}`);
|
|
179
|
-
await upsertSessionDirectory(selectedWorktree.path, Date.now());
|
|
180
|
-
const projectInfo = await getProjectByWorktree(selectedWorktree.path);
|
|
181
|
-
const selectedProjectInfo = { ...projectInfo, name: selectedWorktree.path };
|
|
182
|
-
const replyKeyboard = deps.ensureEventSubscription
|
|
183
|
-
? await switchToProject(ctx, selectedProjectInfo, "worktree_switched", {
|
|
184
|
-
ensureEventSubscription: deps.ensureEventSubscription,
|
|
185
|
-
})
|
|
186
|
-
: await switchToProject(ctx, selectedProjectInfo, "worktree_switched");
|
|
187
|
-
await ctx.answerCallbackQuery();
|
|
188
|
-
await ctx.reply(t("worktree.selected", { worktree: selectedWorktree.path }), {
|
|
189
|
-
reply_markup: replyKeyboard,
|
|
190
|
-
});
|
|
191
|
-
await ctx.deleteMessage();
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
logger.error("[Bot] Error handling worktree callback:", error);
|
|
196
|
-
clearAllInteractionState("worktree_select_error");
|
|
197
|
-
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
198
|
-
await ctx.reply(t("worktree.select_error"));
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
}
|