@grinev/opencode-telegram-bot 0.16.0 → 0.17.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 +8 -0
- package/README.md +72 -42
- package/dist/app/start-bot-app.js +76 -8
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/opencode-start.js +27 -24
- package/dist/bot/commands/opencode-stop.js +51 -23
- package/dist/bot/commands/projects.js +30 -6
- package/dist/bot/commands/skills.js +376 -0
- package/dist/bot/commands/status.js +21 -14
- package/dist/bot/commands/worktree.js +196 -0
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/handlers/voice.js +8 -1
- package/dist/bot/index.js +39 -4
- package/dist/cli/args.js +36 -7
- package/dist/cli.js +131 -17
- package/dist/config.js +3 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +39 -11
- package/dist/i18n/en.js +39 -11
- package/dist/i18n/es.js +39 -11
- package/dist/i18n/fr.js +39 -11
- package/dist/i18n/ru.js +39 -11
- package/dist/i18n/zh.js +39 -11
- package/dist/opencode/process.js +182 -0
- package/dist/pinned/manager.js +28 -61
- package/dist/project/manager.js +47 -32
- package/dist/runtime/bootstrap.js +119 -7
- package/dist/scheduled-task/executor.js +137 -7
- package/dist/scheduled-task/runtime.js +8 -0
- package/dist/service/manager.js +244 -0
- package/dist/service/runtime.js +19 -0
- package/dist/settings/manager.js +9 -12
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- /package/dist/{process → service}/types.js +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
3
|
+
import { getGitWorktreeContext } from "../../git/worktree.js";
|
|
3
4
|
import { getProjects } from "../../project/manager.js";
|
|
4
5
|
import { syncSessionDirectoryCache } from "../../session/cache-manager.js";
|
|
5
6
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
@@ -68,14 +69,37 @@ function buildProjectsMenuText(currentProjectName, page, totalPages) {
|
|
|
68
69
|
total: String(totalPages),
|
|
69
70
|
})}`;
|
|
70
71
|
}
|
|
71
|
-
function
|
|
72
|
+
function worktreeKey(worktree) {
|
|
73
|
+
return process.platform === "win32" ? worktree.toLowerCase() : worktree;
|
|
74
|
+
}
|
|
75
|
+
async function getActiveProjectWorktree() {
|
|
76
|
+
const currentProject = getCurrentProject();
|
|
77
|
+
if (!currentProject) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const worktreeContext = await getGitWorktreeContext(currentProject.worktree);
|
|
82
|
+
if (worktreeContext) {
|
|
83
|
+
return worktreeContext.mainProjectPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.debug("[Projects] Could not resolve active git worktree metadata:", error);
|
|
88
|
+
}
|
|
89
|
+
return currentProject.worktree;
|
|
90
|
+
}
|
|
91
|
+
async function buildProjectsKeyboard(projects, page) {
|
|
72
92
|
const keyboard = new InlineKeyboard();
|
|
73
93
|
const currentProject = getCurrentProject();
|
|
94
|
+
const activeProjectWorktree = await getActiveProjectWorktree();
|
|
74
95
|
const pageSize = config.bot.projectsListLimit;
|
|
75
96
|
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(projects.length, page, pageSize);
|
|
76
97
|
projects.slice(startIndex, endIndex).forEach((project, index) => {
|
|
77
98
|
const isActive = currentProject &&
|
|
78
|
-
(project.id === currentProject.id ||
|
|
99
|
+
(project.id === currentProject.id ||
|
|
100
|
+
project.worktree === currentProject.worktree ||
|
|
101
|
+
(activeProjectWorktree !== null &&
|
|
102
|
+
worktreeKey(project.worktree) === worktreeKey(activeProjectWorktree)));
|
|
79
103
|
const label = buildProjectButtonLabel(startIndex + index, project.worktree);
|
|
80
104
|
const labelWithCheck = formatProjectButtonLabel(label, Boolean(isActive));
|
|
81
105
|
keyboard.text(labelWithCheck, `project:${project.id}`).row();
|
|
@@ -90,14 +114,14 @@ function buildProjectsKeyboard(projects, page) {
|
|
|
90
114
|
}
|
|
91
115
|
return keyboard;
|
|
92
116
|
}
|
|
93
|
-
function buildProjectsMenuView(projects, page) {
|
|
117
|
+
async function buildProjectsMenuView(projects, page) {
|
|
94
118
|
const currentProject = getCurrentProject();
|
|
95
119
|
const pageSize = config.bot.projectsListLimit;
|
|
96
120
|
const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(projects.length, page, pageSize);
|
|
97
121
|
const currentProjectName = currentProject?.name || currentProject?.worktree || null;
|
|
98
122
|
return {
|
|
99
123
|
text: buildProjectsMenuText(currentProjectName, normalizedPage, totalPages),
|
|
100
|
-
keyboard: buildProjectsKeyboard(projects, normalizedPage),
|
|
124
|
+
keyboard: await buildProjectsKeyboard(projects, normalizedPage),
|
|
101
125
|
};
|
|
102
126
|
}
|
|
103
127
|
export async function projectsCommand(ctx) {
|
|
@@ -112,7 +136,7 @@ export async function projectsCommand(ctx) {
|
|
|
112
136
|
await ctx.reply(t("projects.empty"));
|
|
113
137
|
return;
|
|
114
138
|
}
|
|
115
|
-
const { text, keyboard } = buildProjectsMenuView(projects, 0);
|
|
139
|
+
const { text, keyboard } = await buildProjectsMenuView(projects, 0);
|
|
116
140
|
await replyWithInlineMenu(ctx, {
|
|
117
141
|
menuKind: "project",
|
|
118
142
|
text,
|
|
@@ -146,7 +170,7 @@ export async function handleProjectSelect(ctx) {
|
|
|
146
170
|
await ctx.reply(t("projects.empty"));
|
|
147
171
|
return true;
|
|
148
172
|
}
|
|
149
|
-
const { text, keyboard } = buildProjectsMenuView(projects, page);
|
|
173
|
+
const { text, keyboard } = await buildProjectsMenuView(projects, page);
|
|
150
174
|
await ctx.answerCallbackQuery();
|
|
151
175
|
await ctx.editMessageText(text, {
|
|
152
176
|
reply_markup: appendInlineMenuCancelButton(keyboard, "project"),
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
3
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
4
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
import { config } from "../../config.js";
|
|
8
|
+
import { processUserPrompt } from "../handlers/prompt.js";
|
|
9
|
+
const SKILLS_CALLBACK_PREFIX = "skills:";
|
|
10
|
+
const SKILLS_CALLBACK_SELECT_PREFIX = `${SKILLS_CALLBACK_PREFIX}select:`;
|
|
11
|
+
const SKILLS_CALLBACK_PAGE_PREFIX = `${SKILLS_CALLBACK_PREFIX}page:`;
|
|
12
|
+
const SKILLS_CALLBACK_CANCEL = `${SKILLS_CALLBACK_PREFIX}cancel`;
|
|
13
|
+
const SKILLS_CALLBACK_EXECUTE = `${SKILLS_CALLBACK_PREFIX}execute`;
|
|
14
|
+
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
15
|
+
function formatExecutingSkillMessage(skillName, args) {
|
|
16
|
+
const prefix = t("skills.executing_prefix");
|
|
17
|
+
const skillText = `/${skillName}`;
|
|
18
|
+
const argsSuffix = args ? ` ${args}` : "";
|
|
19
|
+
return {
|
|
20
|
+
text: `${prefix}\n${skillText}${argsSuffix}`,
|
|
21
|
+
entities: [
|
|
22
|
+
{
|
|
23
|
+
type: "code",
|
|
24
|
+
offset: prefix.length + 1,
|
|
25
|
+
length: skillText.length,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function buildSkillPageCallback(page) {
|
|
31
|
+
return `${SKILLS_CALLBACK_PAGE_PREFIX}${page}`;
|
|
32
|
+
}
|
|
33
|
+
export function parseSkillPageCallback(data) {
|
|
34
|
+
if (!data.startsWith(SKILLS_CALLBACK_PAGE_PREFIX)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const rawPage = data.slice(SKILLS_CALLBACK_PAGE_PREFIX.length);
|
|
38
|
+
const page = Number(rawPage);
|
|
39
|
+
if (!Number.isInteger(page) || page < 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return page;
|
|
43
|
+
}
|
|
44
|
+
export function formatSkillsSelectText(page) {
|
|
45
|
+
if (page === 0) {
|
|
46
|
+
return t("skills.select");
|
|
47
|
+
}
|
|
48
|
+
return t("skills.select_page", { page: page + 1 });
|
|
49
|
+
}
|
|
50
|
+
function normalizeDirectoryForCommandApi(directory) {
|
|
51
|
+
return directory.replace(/\\/g, "/");
|
|
52
|
+
}
|
|
53
|
+
function getCallbackMessageId(ctx) {
|
|
54
|
+
const message = ctx.callbackQuery?.message;
|
|
55
|
+
if (!message || !("message_id" in message)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const messageId = message.message_id;
|
|
59
|
+
return typeof messageId === "number" ? messageId : null;
|
|
60
|
+
}
|
|
61
|
+
function formatSkillButtonLabel(skill) {
|
|
62
|
+
const description = skill.description?.trim() || t("skills.no_description");
|
|
63
|
+
const rawLabel = `/${skill.name} - ${description}`;
|
|
64
|
+
if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
|
|
65
|
+
return rawLabel;
|
|
66
|
+
}
|
|
67
|
+
return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
|
|
68
|
+
}
|
|
69
|
+
export function calculateSkillsPaginationRange(totalSkills, page, pageSize) {
|
|
70
|
+
const safePageSize = Math.max(1, pageSize);
|
|
71
|
+
const totalPages = Math.max(1, Math.ceil(totalSkills / safePageSize));
|
|
72
|
+
const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
|
|
73
|
+
const startIndex = normalizedPage * safePageSize;
|
|
74
|
+
const endIndex = Math.min(startIndex + safePageSize, totalSkills);
|
|
75
|
+
return {
|
|
76
|
+
page: normalizedPage,
|
|
77
|
+
totalPages,
|
|
78
|
+
startIndex,
|
|
79
|
+
endIndex,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildSkillsListKeyboard(skills, page, pageSize) {
|
|
83
|
+
const keyboard = new InlineKeyboard();
|
|
84
|
+
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateSkillsPaginationRange(skills.length, page, pageSize);
|
|
85
|
+
skills.slice(startIndex, endIndex).forEach((skill, index) => {
|
|
86
|
+
const globalIndex = startIndex + index;
|
|
87
|
+
keyboard
|
|
88
|
+
.text(formatSkillButtonLabel(skill), `${SKILLS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
|
|
89
|
+
.row();
|
|
90
|
+
});
|
|
91
|
+
if (totalPages > 1) {
|
|
92
|
+
if (normalizedPage > 0) {
|
|
93
|
+
keyboard.text(t("skills.button.prev_page"), buildSkillPageCallback(normalizedPage - 1));
|
|
94
|
+
}
|
|
95
|
+
if (normalizedPage < totalPages - 1) {
|
|
96
|
+
keyboard.text(t("skills.button.next_page"), buildSkillPageCallback(normalizedPage + 1));
|
|
97
|
+
}
|
|
98
|
+
keyboard.row();
|
|
99
|
+
}
|
|
100
|
+
keyboard.text(t("skills.button.cancel"), SKILLS_CALLBACK_CANCEL);
|
|
101
|
+
return keyboard;
|
|
102
|
+
}
|
|
103
|
+
function buildSkillsConfirmKeyboard() {
|
|
104
|
+
return new InlineKeyboard()
|
|
105
|
+
.text(t("skills.button.execute"), SKILLS_CALLBACK_EXECUTE)
|
|
106
|
+
.text(t("skills.button.cancel"), SKILLS_CALLBACK_CANCEL);
|
|
107
|
+
}
|
|
108
|
+
function parseSkillItems(value) {
|
|
109
|
+
if (!Array.isArray(value)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const skills = [];
|
|
113
|
+
for (const item of value) {
|
|
114
|
+
if (!item || typeof item !== "object") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const skillName = item.name;
|
|
118
|
+
if (typeof skillName !== "string" || !skillName.trim()) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const description = item.description;
|
|
122
|
+
skills.push({
|
|
123
|
+
name: skillName,
|
|
124
|
+
description: typeof description === "string" ? description : undefined,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return skills;
|
|
128
|
+
}
|
|
129
|
+
function parseSkillsMetadata(state) {
|
|
130
|
+
if (!state || state.kind !== "custom") {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const flow = state.metadata.flow;
|
|
134
|
+
const stage = state.metadata.stage;
|
|
135
|
+
const messageId = state.metadata.messageId;
|
|
136
|
+
const projectDirectory = state.metadata.projectDirectory;
|
|
137
|
+
if (flow !== "skills" || typeof messageId !== "number" || typeof projectDirectory !== "string") {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (stage === "list") {
|
|
141
|
+
const skills = parseSkillItems(state.metadata.skills);
|
|
142
|
+
if (!skills) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const page = typeof state.metadata.page === "number" && Number.isInteger(state.metadata.page)
|
|
146
|
+
? Math.max(0, state.metadata.page)
|
|
147
|
+
: 0;
|
|
148
|
+
return {
|
|
149
|
+
flow,
|
|
150
|
+
stage,
|
|
151
|
+
messageId,
|
|
152
|
+
projectDirectory,
|
|
153
|
+
skills,
|
|
154
|
+
page,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (stage === "confirm") {
|
|
158
|
+
const skillName = state.metadata.skillName;
|
|
159
|
+
if (typeof skillName !== "string" || !skillName.trim()) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
flow,
|
|
164
|
+
stage,
|
|
165
|
+
messageId,
|
|
166
|
+
projectDirectory,
|
|
167
|
+
skillName,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function clearSkillsInteraction(reason) {
|
|
173
|
+
const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
|
|
174
|
+
if (metadata) {
|
|
175
|
+
interactionManager.clear(reason);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function getSkillList(projectDirectory) {
|
|
179
|
+
const { data, error } = await opencodeClient.command.list({
|
|
180
|
+
directory: normalizeDirectoryForCommandApi(projectDirectory),
|
|
181
|
+
});
|
|
182
|
+
if (error || !data) {
|
|
183
|
+
throw error || new Error("No skill data received");
|
|
184
|
+
}
|
|
185
|
+
return data
|
|
186
|
+
.filter((skill) => {
|
|
187
|
+
const source = skill.source;
|
|
188
|
+
return typeof skill.name === "string" && skill.name.trim().length > 0 && source === "skill";
|
|
189
|
+
})
|
|
190
|
+
.map((skill) => ({
|
|
191
|
+
name: skill.name,
|
|
192
|
+
description: skill.description,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
function parseSelectIndex(data) {
|
|
196
|
+
if (!data.startsWith(SKILLS_CALLBACK_SELECT_PREFIX)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const rawIndex = data.slice(SKILLS_CALLBACK_SELECT_PREFIX.length);
|
|
200
|
+
const index = Number(rawIndex);
|
|
201
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return index;
|
|
205
|
+
}
|
|
206
|
+
async function executeSkill(ctx, deps, params) {
|
|
207
|
+
const currentProject = getCurrentProject();
|
|
208
|
+
if (!currentProject) {
|
|
209
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (currentProject.worktree !== params.projectDirectory) {
|
|
213
|
+
logger.warn(`[Skills] Project changed between selection and execution. listedProject=${params.projectDirectory}, currentProject=${currentProject.worktree}. Using current project.`);
|
|
214
|
+
}
|
|
215
|
+
const args = params.argumentsText.trim();
|
|
216
|
+
const executingMessage = formatExecutingSkillMessage(params.skillName, args);
|
|
217
|
+
await ctx.reply(executingMessage.text, { entities: executingMessage.entities });
|
|
218
|
+
const promptText = args ? `/${params.skillName} ${args}` : `/${params.skillName}`;
|
|
219
|
+
await processUserPrompt(ctx, promptText, deps);
|
|
220
|
+
}
|
|
221
|
+
export async function skillsCommand(ctx) {
|
|
222
|
+
try {
|
|
223
|
+
const currentProject = getCurrentProject();
|
|
224
|
+
if (!currentProject) {
|
|
225
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const skills = await getSkillList(currentProject.worktree);
|
|
229
|
+
if (skills.length === 0) {
|
|
230
|
+
await ctx.reply(t("skills.empty"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const pageSize = config.bot.commandsListLimit;
|
|
234
|
+
const keyboard = buildSkillsListKeyboard(skills, 0, pageSize);
|
|
235
|
+
const message = await ctx.reply(formatSkillsSelectText(0), {
|
|
236
|
+
reply_markup: keyboard,
|
|
237
|
+
});
|
|
238
|
+
interactionManager.start({
|
|
239
|
+
kind: "custom",
|
|
240
|
+
expectedInput: "callback",
|
|
241
|
+
metadata: {
|
|
242
|
+
flow: "skills",
|
|
243
|
+
stage: "list",
|
|
244
|
+
messageId: message.message_id,
|
|
245
|
+
projectDirectory: currentProject.worktree,
|
|
246
|
+
skills,
|
|
247
|
+
page: 0,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
logger.error("[Skills] Error fetching skills list:", error);
|
|
253
|
+
await ctx.reply(t("skills.fetch_error"));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export async function handleSkillsCallback(ctx, deps) {
|
|
257
|
+
const data = ctx.callbackQuery?.data;
|
|
258
|
+
if (!data || !data.startsWith(SKILLS_CALLBACK_PREFIX)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
|
|
262
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
263
|
+
if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
|
|
264
|
+
await ctx.answerCallbackQuery({ text: t("skills.inactive_callback"), show_alert: true });
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
if (data === SKILLS_CALLBACK_CANCEL) {
|
|
269
|
+
clearSkillsInteraction("skills_cancelled");
|
|
270
|
+
await ctx.answerCallbackQuery({ text: t("skills.cancelled_callback") });
|
|
271
|
+
await ctx.deleteMessage().catch(() => { });
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (data === SKILLS_CALLBACK_EXECUTE) {
|
|
275
|
+
if (metadata.stage !== "confirm") {
|
|
276
|
+
await ctx.answerCallbackQuery({ text: t("skills.inactive_callback"), show_alert: true });
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
clearSkillsInteraction("skills_execute_clicked");
|
|
280
|
+
await ctx.answerCallbackQuery({ text: t("skills.execute_callback") });
|
|
281
|
+
await ctx.deleteMessage().catch(() => { });
|
|
282
|
+
await executeSkill(ctx, deps, {
|
|
283
|
+
projectDirectory: metadata.projectDirectory,
|
|
284
|
+
skillName: metadata.skillName,
|
|
285
|
+
argumentsText: "",
|
|
286
|
+
});
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
const page = parseSkillPageCallback(data);
|
|
290
|
+
if (page !== null) {
|
|
291
|
+
if (metadata.stage !== "list") {
|
|
292
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
const pageSize = config.bot.commandsListLimit;
|
|
296
|
+
const { page: normalizedPage, totalPages } = calculateSkillsPaginationRange(metadata.skills.length, page, pageSize);
|
|
297
|
+
if (page >= totalPages || page < 0) {
|
|
298
|
+
await ctx.answerCallbackQuery({ text: t("skills.page_empty_callback") });
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const keyboard = buildSkillsListKeyboard(metadata.skills, normalizedPage, pageSize);
|
|
302
|
+
await ctx.editMessageText(formatSkillsSelectText(normalizedPage), {
|
|
303
|
+
reply_markup: keyboard,
|
|
304
|
+
});
|
|
305
|
+
await ctx.answerCallbackQuery();
|
|
306
|
+
interactionManager.transition({
|
|
307
|
+
expectedInput: "callback",
|
|
308
|
+
metadata: {
|
|
309
|
+
flow: "skills",
|
|
310
|
+
stage: "list",
|
|
311
|
+
messageId: metadata.messageId,
|
|
312
|
+
projectDirectory: metadata.projectDirectory,
|
|
313
|
+
skills: metadata.skills,
|
|
314
|
+
page: normalizedPage,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
const skillIndex = parseSelectIndex(data);
|
|
320
|
+
if (skillIndex === null || metadata.stage !== "list") {
|
|
321
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const selectedSkill = metadata.skills[skillIndex];
|
|
325
|
+
if (!selectedSkill) {
|
|
326
|
+
await ctx.answerCallbackQuery({ text: t("skills.inactive_callback"), show_alert: true });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
await ctx.answerCallbackQuery();
|
|
330
|
+
await ctx.editMessageText(t("skills.confirm", { skill: `/${selectedSkill.name}` }), {
|
|
331
|
+
reply_markup: buildSkillsConfirmKeyboard(),
|
|
332
|
+
});
|
|
333
|
+
interactionManager.transition({
|
|
334
|
+
expectedInput: "mixed",
|
|
335
|
+
metadata: {
|
|
336
|
+
flow: "skills",
|
|
337
|
+
stage: "confirm",
|
|
338
|
+
messageId: metadata.messageId,
|
|
339
|
+
projectDirectory: metadata.projectDirectory,
|
|
340
|
+
skillName: selectedSkill.name,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
logger.error("[Skills] Error handling skill callback:", error);
|
|
347
|
+
clearSkillsInteraction("skills_callback_error");
|
|
348
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export async function handleSkillTextArguments(ctx, deps) {
|
|
353
|
+
const text = ctx.message?.text;
|
|
354
|
+
if (!text || text.startsWith("/")) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
|
|
358
|
+
if (!metadata || metadata.stage !== "confirm") {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const argumentsText = text.trim();
|
|
362
|
+
if (!argumentsText) {
|
|
363
|
+
await ctx.reply(t("skills.arguments_empty"));
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
clearSkillsInteraction("skills_arguments_submitted");
|
|
367
|
+
if (ctx.chat) {
|
|
368
|
+
await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
|
|
369
|
+
}
|
|
370
|
+
await executeSkill(ctx, deps, {
|
|
371
|
+
projectDirectory: metadata.projectDirectory,
|
|
372
|
+
skillName: metadata.skillName,
|
|
373
|
+
argumentsText,
|
|
374
|
+
});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { opencodeClient } from "../../opencode/client.js";
|
|
2
|
+
import { getGitWorktreeContext } from "../../git/worktree.js";
|
|
2
3
|
import { getCurrentSession } from "../../session/manager.js";
|
|
3
4
|
import { getCurrentProject, isTtsEnabled } from "../../settings/manager.js";
|
|
4
5
|
import { fetchCurrentAgent } from "../../agent/manager.js";
|
|
5
6
|
import { getAgentDisplayName } from "../../agent/types.js";
|
|
6
7
|
import { fetchCurrentModel } from "../../model/manager.js";
|
|
7
|
-
import { processManager } from "../../process/manager.js";
|
|
8
8
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
9
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
@@ -25,17 +25,6 @@ export async function statusCommand(ctx) {
|
|
|
25
25
|
message += `${t("status.line.tts", {
|
|
26
26
|
tts: isTtsEnabled() ? t("status.tts.on") : t("status.tts.off"),
|
|
27
27
|
})}\n`;
|
|
28
|
-
// Add process management information
|
|
29
|
-
if (processManager.isRunning()) {
|
|
30
|
-
const uptime = processManager.getUptime();
|
|
31
|
-
const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
|
|
32
|
-
message += `${t("status.line.managed_yes")}\n`;
|
|
33
|
-
message += `${t("status.line.pid", { pid: processManager.getPID() ?? "-" })}\n`;
|
|
34
|
-
message += `${t("status.line.uptime_sec", { seconds: uptimeStr })}\n`;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
message += `${t("status.line.managed_no")}\n`;
|
|
38
|
-
}
|
|
39
28
|
// Add agent information
|
|
40
29
|
const currentAgent = await fetchCurrentAgent();
|
|
41
30
|
const agentDisplay = currentAgent
|
|
@@ -48,8 +37,26 @@ export async function statusCommand(ctx) {
|
|
|
48
37
|
message += `${t("status.line.model", { model: modelDisplay })}\n`;
|
|
49
38
|
const currentProject = getCurrentProject();
|
|
50
39
|
if (currentProject) {
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
let projectDisplay = currentProject.worktree;
|
|
41
|
+
let linkedWorktreePath = null;
|
|
42
|
+
try {
|
|
43
|
+
const worktreeContext = await getGitWorktreeContext(currentProject.worktree);
|
|
44
|
+
if (worktreeContext) {
|
|
45
|
+
projectDisplay = worktreeContext.branch
|
|
46
|
+
? `${worktreeContext.mainProjectPath}: ${worktreeContext.branch}`
|
|
47
|
+
: worktreeContext.mainProjectPath;
|
|
48
|
+
linkedWorktreePath = worktreeContext.isLinkedWorktree
|
|
49
|
+
? worktreeContext.activeWorktreePath
|
|
50
|
+
: null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger.debug("[Status] Could not resolve git worktree metadata", error);
|
|
55
|
+
}
|
|
56
|
+
message += `\n${t("status.project_selected", { project: projectDisplay })}\n`;
|
|
57
|
+
if (linkedWorktreePath) {
|
|
58
|
+
message += `${t("status.worktree_selected", { worktree: linkedWorktreePath })}\n`;
|
|
59
|
+
}
|
|
53
60
|
}
|
|
54
61
|
else {
|
|
55
62
|
message += `\n${t("status.project_not_selected")}\n`;
|