lazy-gravity 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/bin/commands/doctor.js +45 -7
- package/dist/bot/index.js +526 -24
- package/dist/commands/chatCommandHandler.js +11 -41
- package/dist/commands/registerSlashCommands.js +62 -0
- package/dist/database/scheduleRepository.js +50 -6
- package/dist/events/interactionCreateHandler.js +163 -28
- package/dist/events/messageCreateHandler.js +24 -28
- package/dist/handlers/fileChangeButtonAction.js +12 -24
- package/dist/handlers/genericActionButtonAction.js +16 -18
- package/dist/handlers/planningButtonAction.js +105 -17
- package/dist/handlers/planningModalSubmitAction.js +38 -0
- package/dist/handlers/questionSelectAction.js +8 -7
- package/dist/handlers/questionSkipAction.js +7 -6
- package/dist/platform/discord/wrappers.js +76 -0
- package/dist/services/approvalDetector.js +43 -14
- package/dist/services/artifactService.js +61 -21
- package/dist/services/assistantDomExtractor.js +23 -3
- package/dist/services/cdpBridgeManager.js +35 -2
- package/dist/services/cdpConnectionPool.js +7 -2
- package/dist/services/cdpService.js +113 -8
- package/dist/services/chatSessionService.js +15 -8
- package/dist/services/heartbeatService.js +261 -0
- package/dist/services/notificationSender.js +12 -2
- package/dist/services/planningDetector.js +1 -1
- package/dist/services/promptDispatcher.js +21 -1
- package/dist/services/questionDetector.js +128 -76
- package/dist/services/responseMonitor.js +17 -1
- package/dist/services/scheduleService.js +101 -4
- package/dist/utils/configLoader.js +8 -0
- package/dist/utils/fileOpenCache.js +2 -6
- package/dist/utils/questionActionUtils.js +22 -0
- package/package.json +2 -1
|
@@ -1,40 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.ChatCommandHandler = void 0;
|
|
37
4
|
const i18n_1 = require("../utils/i18n");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
38
6
|
const discord_js_1 = require("discord.js");
|
|
39
7
|
/**
|
|
40
8
|
* Handler for chat session related commands
|
|
@@ -111,9 +79,7 @@ class ChatCommandHandler {
|
|
|
111
79
|
if (!newChatResult.ok) {
|
|
112
80
|
// Log but don't fail the command, as the channel still needs to be created
|
|
113
81
|
// and the user might just have to click it manually if the IDE state is strange.
|
|
114
|
-
|
|
115
|
-
logger.warn(`[/new] Could not start new chat in IDE automatically: ${newChatResult.error}`);
|
|
116
|
-
});
|
|
82
|
+
logger_1.logger.warn(`[/new] Could not start new chat in IDE automatically: ${newChatResult.error}`);
|
|
117
83
|
}
|
|
118
84
|
const customNameRaw = interaction.options.getString('name');
|
|
119
85
|
let safeCustomName = null;
|
|
@@ -156,9 +122,7 @@ class ChatCommandHandler {
|
|
|
156
122
|
if (workspaceCdp) {
|
|
157
123
|
const renameResult = await this.chatSessionService.renameCurrentChatInUI(workspaceCdp, safeCustomName);
|
|
158
124
|
if (!renameResult.ok) {
|
|
159
|
-
|
|
160
|
-
logger.warn(`[/new] Could not rename chat in IDE automatically: ${renameResult.error}`);
|
|
161
|
-
});
|
|
125
|
+
logger_1.logger.warn(`[/new] Could not rename chat in IDE automatically: ${renameResult.error}`);
|
|
162
126
|
}
|
|
163
127
|
}
|
|
164
128
|
}
|
|
@@ -185,8 +149,14 @@ class ChatCommandHandler {
|
|
|
185
149
|
const embed = new discord_js_1.EmbedBuilder()
|
|
186
150
|
.setTitle((0, i18n_1.t)('💬 Chat Session Info'))
|
|
187
151
|
.setColor(info.hasActiveChat ? 0x00CC88 : 0x888888)
|
|
188
|
-
.addFields({ name: (0, i18n_1.t)('Title'), value: info.title, inline: true }, { name: (0, i18n_1.t)('Status'), value: info.hasActiveChat ? (0, i18n_1.t)('🟢 Active') : (0, i18n_1.t)('⚪ Inactive'), inline: true })
|
|
189
|
-
|
|
152
|
+
.addFields({ name: (0, i18n_1.t)('Title'), value: info.title, inline: true }, { name: (0, i18n_1.t)('Status'), value: info.hasActiveChat ? (0, i18n_1.t)('🟢 Active') : (0, i18n_1.t)('⚪ Inactive'), inline: true });
|
|
153
|
+
const channel = await interaction.client.channels.fetch(interaction.channelId).catch(() => null);
|
|
154
|
+
const categoryId = channel?.parentId;
|
|
155
|
+
const isProjectChannel = categoryId ? !!this.bindingRepo.findByChannelId(categoryId) : false;
|
|
156
|
+
const desc = isProjectChannel
|
|
157
|
+
? (0, i18n_1.t)('※ No active session is bound to this channel.\n\n💡 **Tip**: If you recently deleted a session in the IDE, this channel was automatically unbound. Send a prompt or use `/new` to start a fresh chat.')
|
|
158
|
+
: (0, i18n_1.t)('※ Non-session channel.\nUse `/project` to create a project first.');
|
|
159
|
+
embed.setDescription(desc)
|
|
190
160
|
.setTimestamp();
|
|
191
161
|
await interaction.editReply({ embeds: [embed] });
|
|
192
162
|
return;
|
|
@@ -168,6 +168,66 @@ const openCommand = new discord_js_1.SlashCommandBuilder()
|
|
|
168
168
|
.setName('filepath')
|
|
169
169
|
.setDescription((0, i18n_1.t)('Absolute or relative path to the file'))
|
|
170
170
|
.setRequired(true));
|
|
171
|
+
/** /heartbeat command definition */
|
|
172
|
+
const heartbeatCommand = new discord_js_1.SlashCommandBuilder()
|
|
173
|
+
.setName('heartbeat')
|
|
174
|
+
.setDescription((0, i18n_1.t)('Configure periodic bot heartbeat notifications'))
|
|
175
|
+
.addSubcommand((sub) => sub
|
|
176
|
+
.setName('on')
|
|
177
|
+
.setDescription((0, i18n_1.t)('Enable periodic heartbeats'))
|
|
178
|
+
.addStringOption((option) => option
|
|
179
|
+
.setName('interval')
|
|
180
|
+
.setDescription((0, i18n_1.t)('Interval (e.g., 1d, 1h, 30m - unit required)'))
|
|
181
|
+
.setRequired(false))
|
|
182
|
+
.addChannelOption((option) => option
|
|
183
|
+
.setName('channel')
|
|
184
|
+
.setDescription((0, i18n_1.t)('Target channel for heartbeat (defaults to current)'))
|
|
185
|
+
.addChannelTypes(discord_js_1.ChannelType.GuildText, discord_js_1.ChannelType.GuildAnnouncement)
|
|
186
|
+
.setRequired(false)))
|
|
187
|
+
.addSubcommand((sub) => sub
|
|
188
|
+
.setName('off')
|
|
189
|
+
.setDescription((0, i18n_1.t)('Disable periodic heartbeats')))
|
|
190
|
+
.addSubcommand((sub) => sub
|
|
191
|
+
.setName('status')
|
|
192
|
+
.setDescription((0, i18n_1.t)('Display current heartbeat config and status')));
|
|
193
|
+
/** /schedule command definition */
|
|
194
|
+
const scheduleCommand = new discord_js_1.SlashCommandBuilder()
|
|
195
|
+
.setName('schedule')
|
|
196
|
+
.setDescription((0, i18n_1.t)('Manage scheduled tasks'))
|
|
197
|
+
.addSubcommand((sub) => sub
|
|
198
|
+
.setName('list')
|
|
199
|
+
.setDescription((0, i18n_1.t)('Show all scheduled tasks with next-run times')))
|
|
200
|
+
.addSubcommand((sub) => sub
|
|
201
|
+
.setName('add')
|
|
202
|
+
.setDescription((0, i18n_1.t)('Register a recurring task'))
|
|
203
|
+
.addStringOption((option) => option
|
|
204
|
+
.setName('cron')
|
|
205
|
+
.setDescription((0, i18n_1.t)('Cron expression (e.g. "0 * * * *")'))
|
|
206
|
+
.setRequired(true))
|
|
207
|
+
.addStringOption((option) => option
|
|
208
|
+
.setName('prompt')
|
|
209
|
+
.setDescription((0, i18n_1.t)('Prompt content to execute'))
|
|
210
|
+
.setRequired(true)))
|
|
211
|
+
.addSubcommand((sub) => sub
|
|
212
|
+
.setName('remove')
|
|
213
|
+
.setDescription((0, i18n_1.t)('Delete a scheduled task'))
|
|
214
|
+
.addIntegerOption((option) => option
|
|
215
|
+
.setName('id')
|
|
216
|
+
.setDescription((0, i18n_1.t)('ID of the task to delete'))
|
|
217
|
+
.setRequired(true)))
|
|
218
|
+
.addSubcommand((sub) => sub
|
|
219
|
+
.setName('clear')
|
|
220
|
+
.setDescription((0, i18n_1.t)('Remove all scheduled tasks and reset task IDs')))
|
|
221
|
+
.addSubcommand((sub) => sub
|
|
222
|
+
.setName('backup')
|
|
223
|
+
.setDescription((0, i18n_1.t)('Export all scheduled tasks as a JSON file attachment')))
|
|
224
|
+
.addSubcommand((sub) => sub
|
|
225
|
+
.setName('restore')
|
|
226
|
+
.setDescription((0, i18n_1.t)('Restore scheduled tasks from a JSON file attachment'))
|
|
227
|
+
.addAttachmentOption((option) => option
|
|
228
|
+
.setName('file')
|
|
229
|
+
.setDescription((0, i18n_1.t)('The schedules_backup.json file to import'))
|
|
230
|
+
.setRequired(true)));
|
|
171
231
|
/** Array of commands to register */
|
|
172
232
|
exports.slashCommands = [
|
|
173
233
|
helpCommand,
|
|
@@ -191,6 +251,8 @@ exports.slashCommands = [
|
|
|
191
251
|
logsCommand,
|
|
192
252
|
artifactsCommand,
|
|
193
253
|
openCommand,
|
|
254
|
+
heartbeatCommand,
|
|
255
|
+
scheduleCommand,
|
|
194
256
|
];
|
|
195
257
|
/**
|
|
196
258
|
* Register slash commands with Discord
|
|
@@ -11,9 +11,6 @@ class ScheduleRepository {
|
|
|
11
11
|
this.db = db;
|
|
12
12
|
this.initialize();
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Initialize table (create if not exists)
|
|
16
|
-
*/
|
|
17
14
|
initialize() {
|
|
18
15
|
this.db.exec(`
|
|
19
16
|
CREATE TABLE IF NOT EXISTS schedules (
|
|
@@ -21,25 +18,33 @@ class ScheduleRepository {
|
|
|
21
18
|
cron_expression TEXT NOT NULL,
|
|
22
19
|
prompt TEXT NOT NULL,
|
|
23
20
|
workspace_path TEXT NOT NULL,
|
|
21
|
+
channel_id TEXT,
|
|
24
22
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
25
23
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
24
|
)
|
|
27
25
|
`);
|
|
26
|
+
// Perform schema migration if channel_id doesn't exist (fallback for old databases)
|
|
27
|
+
const tableInfo = this.db.pragma("table_info(schedules)");
|
|
28
|
+
const hasChannelId = tableInfo.some((col) => col.name === 'channel_id');
|
|
29
|
+
if (!hasChannelId) {
|
|
30
|
+
this.db.exec('ALTER TABLE schedules ADD COLUMN channel_id TEXT');
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
/**
|
|
30
34
|
* Create a new schedule
|
|
31
35
|
*/
|
|
32
36
|
create(input) {
|
|
33
37
|
const stmt = this.db.prepare(`
|
|
34
|
-
INSERT INTO schedules (cron_expression, prompt, workspace_path, enabled)
|
|
35
|
-
VALUES (?, ?, ?, ?)
|
|
38
|
+
INSERT INTO schedules (cron_expression, prompt, workspace_path, channel_id, enabled)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?)
|
|
36
40
|
`);
|
|
37
|
-
const result = stmt.run(input.cronExpression, input.prompt, input.workspacePath, input.enabled ? 1 : 0);
|
|
41
|
+
const result = stmt.run(input.cronExpression, input.prompt, input.workspacePath, input.channelId || '', input.enabled ? 1 : 0);
|
|
38
42
|
return {
|
|
39
43
|
id: result.lastInsertRowid,
|
|
40
44
|
cronExpression: input.cronExpression,
|
|
41
45
|
prompt: input.prompt,
|
|
42
46
|
workspacePath: input.workspacePath,
|
|
47
|
+
channelId: input.channelId,
|
|
43
48
|
enabled: input.enabled,
|
|
44
49
|
};
|
|
45
50
|
}
|
|
@@ -73,6 +78,40 @@ class ScheduleRepository {
|
|
|
73
78
|
const result = this.db.prepare('DELETE FROM schedules WHERE id = ?').run(id);
|
|
74
79
|
return result.changes > 0;
|
|
75
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear all schedules and reset the autoincrement ID back to 0
|
|
83
|
+
*/
|
|
84
|
+
reset() {
|
|
85
|
+
this.db.transaction(() => {
|
|
86
|
+
this.db.exec('DELETE FROM schedules');
|
|
87
|
+
this.db.exec("DELETE FROM sqlite_sequence WHERE name = 'schedules'");
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Bulk restore schedules. Clears the table and inserts all provided records.
|
|
92
|
+
*/
|
|
93
|
+
bulkRestore(records) {
|
|
94
|
+
const result = [];
|
|
95
|
+
this.db.transaction(() => {
|
|
96
|
+
this.reset();
|
|
97
|
+
const stmt = this.db.prepare(`
|
|
98
|
+
INSERT INTO schedules (cron_expression, prompt, workspace_path, channel_id, enabled)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?)
|
|
100
|
+
`);
|
|
101
|
+
for (const record of records) {
|
|
102
|
+
const insertResult = stmt.run(record.cronExpression, record.prompt, record.workspacePath, record.channelId || '', record.enabled ? 1 : 0);
|
|
103
|
+
result.push({
|
|
104
|
+
id: insertResult.lastInsertRowid,
|
|
105
|
+
cronExpression: record.cronExpression,
|
|
106
|
+
prompt: record.prompt,
|
|
107
|
+
workspacePath: record.workspacePath,
|
|
108
|
+
channelId: record.channelId,
|
|
109
|
+
enabled: record.enabled,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
76
115
|
/**
|
|
77
116
|
* Partially update a schedule
|
|
78
117
|
*/
|
|
@@ -91,6 +130,10 @@ class ScheduleRepository {
|
|
|
91
130
|
sets.push('workspace_path = ?');
|
|
92
131
|
values.push(input.workspacePath);
|
|
93
132
|
}
|
|
133
|
+
if (input.channelId !== undefined) {
|
|
134
|
+
sets.push('channel_id = ?');
|
|
135
|
+
values.push(input.channelId);
|
|
136
|
+
}
|
|
94
137
|
if (input.enabled !== undefined) {
|
|
95
138
|
sets.push('enabled = ?');
|
|
96
139
|
values.push(input.enabled ? 1 : 0);
|
|
@@ -111,6 +154,7 @@ class ScheduleRepository {
|
|
|
111
154
|
cronExpression: row.cron_expression,
|
|
112
155
|
prompt: row.prompt,
|
|
113
156
|
workspacePath: row.workspace_path,
|
|
157
|
+
channelId: row.channel_id || undefined,
|
|
114
158
|
enabled: row.enabled === 1,
|
|
115
159
|
createdAt: row.created_at,
|
|
116
160
|
};
|
|
@@ -21,6 +21,8 @@ const notificationSender_1 = require("../services/notificationSender");
|
|
|
21
21
|
const questionSelectAction_1 = require("../handlers/questionSelectAction");
|
|
22
22
|
const questionSkipAction_1 = require("../handlers/questionSkipAction");
|
|
23
23
|
const wrappers_1 = require("../platform/discord/wrappers");
|
|
24
|
+
const questionActionUtils_1 = require("../utils/questionActionUtils");
|
|
25
|
+
const projectResolver_1 = require("../utils/projectResolver");
|
|
24
26
|
const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
|
|
25
27
|
const projectListUi_1 = require("../ui/projectListUi");
|
|
26
28
|
const modeService_1 = require("../services/modeService");
|
|
@@ -110,6 +112,10 @@ function createInteractionCreateHandler(deps) {
|
|
|
110
112
|
return { ok: true };
|
|
111
113
|
};
|
|
112
114
|
return async (interaction) => {
|
|
115
|
+
const isHeartbeatCommand = 'commandName' in interaction && interaction.commandName === 'heartbeat';
|
|
116
|
+
if (deps.heartbeatService && deps.config.allowedUserIds.includes(interaction.user.id) && !isHeartbeatCommand) {
|
|
117
|
+
deps.heartbeatService.recordActivity();
|
|
118
|
+
}
|
|
113
119
|
if (interaction.isAutocomplete()) {
|
|
114
120
|
if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
|
|
115
121
|
await interaction.respond([]).catch(logger_1.logger.error);
|
|
@@ -226,6 +232,19 @@ function createInteractionCreateHandler(deps) {
|
|
|
226
232
|
embeds: [updatedEmbed],
|
|
227
233
|
components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
|
|
228
234
|
});
|
|
235
|
+
if (success && (approvalAction.action === 'approve' || approvalAction.action === 'always_allow') && deps.promptDispatcher && interaction.channelId && interaction.message) {
|
|
236
|
+
const cdp = projectName
|
|
237
|
+
? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
|
|
238
|
+
: null;
|
|
239
|
+
if (cdp) {
|
|
240
|
+
deps.promptDispatcher.resume({
|
|
241
|
+
message: interaction.message,
|
|
242
|
+
prompt: '',
|
|
243
|
+
cdp,
|
|
244
|
+
options: buildResumeOptions()
|
|
245
|
+
}).catch(e => logger_1.logger.error('[Approval] Failed to resume monitoring:', e));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
229
248
|
}
|
|
230
249
|
else {
|
|
231
250
|
await interaction.reply({ content: 'Approval button not found.', flags: discord_js_1.MessageFlags.Ephemeral });
|
|
@@ -873,8 +892,39 @@ function createInteractionCreateHandler(deps) {
|
|
|
873
892
|
}
|
|
874
893
|
if (interaction.customId.startsWith('file_open:')) {
|
|
875
894
|
await interaction.deferUpdate().catch(() => { });
|
|
876
|
-
|
|
877
|
-
const
|
|
895
|
+
let fileUrl = undefined;
|
|
896
|
+
const suffix = interaction.customId.substring('file_open:'.length);
|
|
897
|
+
if (suffix.startsWith('rel:')) {
|
|
898
|
+
const relativePath = suffix.substring('rel:'.length);
|
|
899
|
+
const workspacePath = interaction.channelId ? (deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
|
|
900
|
+
deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath) : undefined;
|
|
901
|
+
if (workspacePath) {
|
|
902
|
+
fileUrl = `file:///${path_1.default.join(workspacePath, relativePath).replace(/\\/g, '/')}`;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else if (suffix.startsWith('art:')) {
|
|
906
|
+
const parts = suffix.substring('art:'.length).split(':');
|
|
907
|
+
if (parts.length >= 2) {
|
|
908
|
+
const convId = parts[0];
|
|
909
|
+
const filename = parts.slice(1).join(':');
|
|
910
|
+
if (deps.artifactService) {
|
|
911
|
+
const artifacts = deps.artifactService.listArtifacts(convId);
|
|
912
|
+
const matched = artifacts.find(art => art.filename.toLowerCase() === filename.toLowerCase());
|
|
913
|
+
if (matched) {
|
|
914
|
+
fileUrl = `file:///${matched.absolutePath.replace(/\\/g, '/')}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!fileUrl) {
|
|
918
|
+
const brainDir = path_1.default.join(require('os').homedir(), '.gemini', 'antigravity', 'brain');
|
|
919
|
+
fileUrl = `file:///${path_1.default.join(brainDir, convId, filename).replace(/\\/g, '/')}`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
// Fallback to cache (for legacy file_open:<hashId> or explicit cache usage)
|
|
925
|
+
const hashId = suffix.startsWith('cache:') ? suffix.substring('cache:'.length) : suffix;
|
|
926
|
+
fileUrl = fileOpenCache_1.fileOpenCache.get(hashId);
|
|
927
|
+
}
|
|
878
928
|
if (!fileUrl) {
|
|
879
929
|
await interaction.followUp({
|
|
880
930
|
content: (0, i18n_1.t)('File URL not found in cache or expired.'),
|
|
@@ -893,37 +943,58 @@ function createInteractionCreateHandler(deps) {
|
|
|
893
943
|
}
|
|
894
944
|
resolvedPath = path_1.default.resolve(rawPath);
|
|
895
945
|
}
|
|
896
|
-
|
|
897
|
-
if (
|
|
898
|
-
workspacePath =
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
946
|
+
const isArtifact = resolvedPath.includes('.gemini') && resolvedPath.includes('antigravity') && resolvedPath.includes('brain');
|
|
947
|
+
if (!isArtifact) {
|
|
948
|
+
let workspacePath = undefined;
|
|
949
|
+
if (interaction.channelId) {
|
|
950
|
+
workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
|
|
951
|
+
deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath;
|
|
952
|
+
}
|
|
953
|
+
if (!workspacePath) {
|
|
954
|
+
await interaction.followUp({
|
|
955
|
+
content: `❌ Error: Cannot open files without an active workspace.`,
|
|
956
|
+
flags: discord_js_1.MessageFlags.Ephemeral
|
|
957
|
+
}).catch(logger_1.logger.error);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const resolvedWorkspacePath = path_1.default.resolve(workspacePath);
|
|
961
|
+
const relative = path_1.default.relative(resolvedWorkspacePath, resolvedPath);
|
|
962
|
+
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
|
|
963
|
+
await interaction.followUp({
|
|
964
|
+
content: `❌ Error: Cannot open files outside the workspace root.`,
|
|
965
|
+
flags: discord_js_1.MessageFlags.Ephemeral
|
|
966
|
+
}).catch(logger_1.logger.error);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
915
969
|
}
|
|
916
970
|
try {
|
|
971
|
+
const comp = interaction.component;
|
|
972
|
+
const buttonLabel = comp?.type === 2 ? comp.label : null;
|
|
973
|
+
const isReviewBtn = suffix.startsWith('art:') ||
|
|
974
|
+
buttonLabel?.toLowerCase().includes('review') ||
|
|
975
|
+
resolvedPath.endsWith('walkthrough.md') ||
|
|
976
|
+
resolvedPath.endsWith('task.md') ||
|
|
977
|
+
resolvedPath.endsWith('implementation_plan.md');
|
|
917
978
|
const fileContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
|
|
918
979
|
const MAX_DESC_LEN = 4096;
|
|
919
|
-
|
|
980
|
+
let truncated = fileContent.length > MAX_DESC_LEN
|
|
920
981
|
? fileContent.substring(0, MAX_DESC_LEN - 15) + '\n\n(truncated)'
|
|
921
982
|
: fileContent;
|
|
983
|
+
if (isReviewBtn) {
|
|
984
|
+
truncated += '\n\n💡 *To request changes to this artifact, simply reply in this channel.*';
|
|
985
|
+
}
|
|
922
986
|
const embed = new discord_js_1.EmbedBuilder()
|
|
923
|
-
.setTitle(`Opened: ${path_1.default.basename(resolvedPath)}`)
|
|
987
|
+
.setTitle(isReviewBtn ? `Reviewing: ${path_1.default.basename(resolvedPath)}` : `Opened: ${path_1.default.basename(resolvedPath)}`)
|
|
924
988
|
.setDescription(truncated)
|
|
925
|
-
.setColor(0x3498DB)
|
|
989
|
+
.setColor(isReviewBtn ? 0x2ECC71 : 0x3498DB)
|
|
926
990
|
.setTimestamp();
|
|
991
|
+
if (isReviewBtn) {
|
|
992
|
+
await interaction.followUp({
|
|
993
|
+
embeds: [embed],
|
|
994
|
+
components: []
|
|
995
|
+
}).catch(logger_1.logger.error);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
927
998
|
(0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], async (error) => {
|
|
928
999
|
if (error) {
|
|
929
1000
|
logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
|
|
@@ -933,9 +1004,14 @@ function createInteractionCreateHandler(deps) {
|
|
|
933
1004
|
}).catch(() => { });
|
|
934
1005
|
}
|
|
935
1006
|
else {
|
|
1007
|
+
const closeRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
|
|
1008
|
+
.setCustomId('file_close_active')
|
|
1009
|
+
.setLabel('Close Active Tab in IDE')
|
|
1010
|
+
.setStyle(discord_js_1.ButtonStyle.Secondary));
|
|
936
1011
|
await interaction.followUp({
|
|
937
1012
|
content: `✅ Opened file in IDE: **${path_1.default.basename(resolvedPath)}**`,
|
|
938
1013
|
embeds: [embed],
|
|
1014
|
+
components: [closeRow],
|
|
939
1015
|
flags: discord_js_1.MessageFlags.Ephemeral
|
|
940
1016
|
}).catch(() => { });
|
|
941
1017
|
}
|
|
@@ -949,9 +1025,38 @@ function createInteractionCreateHandler(deps) {
|
|
|
949
1025
|
}
|
|
950
1026
|
return;
|
|
951
1027
|
}
|
|
1028
|
+
if (interaction.customId === 'file_close_active') {
|
|
1029
|
+
await interaction.deferUpdate().catch(() => { });
|
|
1030
|
+
try {
|
|
1031
|
+
const cdp = deps.getCurrentCdp(deps.bridge);
|
|
1032
|
+
if (!cdp) {
|
|
1033
|
+
await interaction.followUp({
|
|
1034
|
+
content: `❌ No active CDP connection found.`,
|
|
1035
|
+
flags: discord_js_1.MessageFlags.Ephemeral
|
|
1036
|
+
}).catch(() => { });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (!cdp.isConnected()) {
|
|
1040
|
+
await cdp.connect();
|
|
1041
|
+
}
|
|
1042
|
+
await cdp.closeActiveEditor();
|
|
1043
|
+
await interaction.followUp({
|
|
1044
|
+
content: `✅ Sent close command to IDE.`,
|
|
1045
|
+
flags: discord_js_1.MessageFlags.Ephemeral
|
|
1046
|
+
}).catch(() => { });
|
|
1047
|
+
}
|
|
1048
|
+
catch (e) {
|
|
1049
|
+
logger_1.logger.error(`Failed to close active editor: ${e.message}`);
|
|
1050
|
+
await interaction.followUp({
|
|
1051
|
+
content: `❌ Error closing editor: ${e.message}`,
|
|
1052
|
+
flags: discord_js_1.MessageFlags.Ephemeral
|
|
1053
|
+
}).catch(() => { });
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
952
1057
|
}
|
|
953
1058
|
catch (error) {
|
|
954
|
-
logger_1.logger.error(
|
|
1059
|
+
logger_1.logger.error(`[InteractionCreate] Error handling interaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
955
1060
|
try {
|
|
956
1061
|
if (!interaction.replied && !interaction.deferred) {
|
|
957
1062
|
await interaction.reply({ content: 'An error occurred while processing the button action.', flags: discord_js_1.MessageFlags.Ephemeral });
|
|
@@ -1243,7 +1348,22 @@ function createInteractionCreateHandler(deps) {
|
|
|
1243
1348
|
try {
|
|
1244
1349
|
const action = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
|
|
1245
1350
|
const wrapped = (0, wrappers_1.wrapDiscordSelect)(interaction);
|
|
1246
|
-
await action.execute(wrapped, interaction.values);
|
|
1351
|
+
const success = await action.execute(wrapped, interaction.values);
|
|
1352
|
+
if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
|
|
1353
|
+
const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
|
|
1354
|
+
if (parsed) {
|
|
1355
|
+
const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
|
|
1356
|
+
const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
|
|
1357
|
+
if (cdp) {
|
|
1358
|
+
deps.promptDispatcher.resume({
|
|
1359
|
+
message: interaction.message,
|
|
1360
|
+
prompt: '',
|
|
1361
|
+
cdp,
|
|
1362
|
+
options: buildResumeOptions()
|
|
1363
|
+
}).catch(e => logger_1.logger.error('[QuestionSelectAction] Failed to resume monitoring:', e));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1247
1367
|
}
|
|
1248
1368
|
catch (err) {
|
|
1249
1369
|
logger_1.logger.error('[QuestionSelectAction] Error executing action:', err);
|
|
@@ -1260,7 +1380,22 @@ function createInteractionCreateHandler(deps) {
|
|
|
1260
1380
|
try {
|
|
1261
1381
|
const action = (0, questionSkipAction_1.createQuestionSkipAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
|
|
1262
1382
|
const wrapped = (0, wrappers_1.wrapDiscordButton)(interaction);
|
|
1263
|
-
await action.execute(wrapped, {});
|
|
1383
|
+
const success = await action.execute(wrapped, {});
|
|
1384
|
+
if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
|
|
1385
|
+
const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
|
|
1386
|
+
if (parsed) {
|
|
1387
|
+
const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
|
|
1388
|
+
const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
|
|
1389
|
+
if (cdp) {
|
|
1390
|
+
deps.promptDispatcher.resume({
|
|
1391
|
+
message: interaction.message,
|
|
1392
|
+
prompt: '',
|
|
1393
|
+
cdp,
|
|
1394
|
+
options: buildResumeOptions()
|
|
1395
|
+
}).catch(e => logger_1.logger.error('[QuestionSkipAction] Failed to resume monitoring:', e));
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1264
1399
|
}
|
|
1265
1400
|
catch (err) {
|
|
1266
1401
|
logger_1.logger.error('[QuestionSkipAction] Error executing action:', err);
|
|
@@ -1317,7 +1452,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
1317
1452
|
throw deferError;
|
|
1318
1453
|
}
|
|
1319
1454
|
try {
|
|
1320
|
-
await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client, deps.accountPrefRepo, deps.channelPrefRepo, deps.antigravityAccounts, deps.chatSessionRepo, deps.scheduleService);
|
|
1455
|
+
await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client, deps.accountPrefRepo, deps.channelPrefRepo, deps.antigravityAccounts, deps.chatSessionRepo, deps.scheduleService, deps.heartbeatService);
|
|
1321
1456
|
}
|
|
1322
1457
|
catch (error) {
|
|
1323
1458
|
logger_1.logger.error(`[SlashCommand] command=${commandInteraction.commandName} channel=${commandInteraction.channelId} ` +
|
|
@@ -10,7 +10,9 @@ const modeService_1 = require("../services/modeService");
|
|
|
10
10
|
const imageHandler_1 = require("../utils/imageHandler");
|
|
11
11
|
const accountUtils_1 = require("../utils/accountUtils");
|
|
12
12
|
const logger_1 = require("../utils/logger");
|
|
13
|
+
const workspaceQueue_1 = require("../bot/workspaceQueue");
|
|
13
14
|
function createMessageCreateHandler(deps) {
|
|
15
|
+
const workspaceQueue = deps.workspaceQueue ?? new workspaceQueue_1.WorkspaceQueue();
|
|
14
16
|
const getCurrentCdp = deps.getCurrentCdp ?? cdpBridgeManager_1.getCurrentCdp;
|
|
15
17
|
const ensureApprovalDetector = deps.ensureApprovalDetector ?? cdpBridgeManager_1.ensureApprovalDetector;
|
|
16
18
|
const ensureErrorPopupDetector = deps.ensureErrorPopupDetector ?? cdpBridgeManager_1.ensureErrorPopupDetector;
|
|
@@ -31,29 +33,15 @@ function createMessageCreateHandler(deps) {
|
|
|
31
33
|
return match ? match.cdpPort : null;
|
|
32
34
|
};
|
|
33
35
|
const getSessionAccountName = (channelId) => deps.chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null;
|
|
34
|
-
// Per-workspace prompt queue: serializes send→response cycles
|
|
35
|
-
const workspaceQueues = new Map();
|
|
36
|
-
const workspaceQueueDepths = new Map();
|
|
37
|
-
function enqueueForWorkspace(workspacePath, task) {
|
|
38
|
-
// .catch: ensure a prior rejection never stalls the chain
|
|
39
|
-
const current = (workspaceQueues.get(workspacePath) ?? Promise.resolve()).catch(() => { });
|
|
40
|
-
const next = current.then(async () => {
|
|
41
|
-
try {
|
|
42
|
-
await task();
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
logger_1.logger.error('[WorkspaceQueue] task error:', err?.message || err);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
workspaceQueues.set(workspacePath, next);
|
|
49
|
-
return next;
|
|
50
|
-
}
|
|
51
36
|
return async (message) => {
|
|
52
37
|
if (message.author.bot)
|
|
53
38
|
return;
|
|
54
39
|
if (!deps.config.allowedUserIds.includes(message.author.id)) {
|
|
55
40
|
return;
|
|
56
41
|
}
|
|
42
|
+
if (deps.heartbeatService) {
|
|
43
|
+
deps.heartbeatService.recordActivity();
|
|
44
|
+
}
|
|
57
45
|
const parsed = (0, messageParser_1.parseMessageContent)(message.content);
|
|
58
46
|
if (parsed.isCommand && parsed.commandName) {
|
|
59
47
|
if (parsed.commandName === 'autoaccept') {
|
|
@@ -160,7 +148,7 @@ function createMessageCreateHandler(deps) {
|
|
|
160
148
|
await message.reply(`✅ Switched session account to **${requested}**.`).catch(() => { });
|
|
161
149
|
return;
|
|
162
150
|
}
|
|
163
|
-
const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup', 'join', 'mirror', 'output'];
|
|
151
|
+
const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup', 'join', 'mirror', 'output', 'heartbeat'];
|
|
164
152
|
if (slashOnlyCommands.includes(parsed.commandName)) {
|
|
165
153
|
await message.reply({
|
|
166
154
|
content: `💡 Please use \`/${parsed.commandName}\` as a slash command.\nType \`/${parsed.commandName}\` in the Discord input field to see suggestions.`,
|
|
@@ -225,7 +213,10 @@ function createMessageCreateHandler(deps) {
|
|
|
225
213
|
const chunk = textAttachments.slice(i, i + CONCURRENCY_LIMIT);
|
|
226
214
|
const results = await Promise.all(chunk.map(async (textAtt) => {
|
|
227
215
|
try {
|
|
228
|
-
const
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
218
|
+
const res = await fetch(textAtt.url, { signal: controller.signal });
|
|
219
|
+
clearTimeout(timeoutId);
|
|
229
220
|
if (res.ok) {
|
|
230
221
|
const content = await res.text();
|
|
231
222
|
return `\n\n[Attached File: ${textAtt.name}]\n\`\`\`\n${content}\n\`\`\``;
|
|
@@ -252,9 +243,8 @@ function createMessageCreateHandler(deps) {
|
|
|
252
243
|
if (workspacePath) {
|
|
253
244
|
const projectLabel = deps.bridge.pool.extractProjectName(workspacePath);
|
|
254
245
|
// Track queue depth for hourglass reactions
|
|
255
|
-
const currentDepth =
|
|
256
|
-
|
|
257
|
-
const newDepth = currentDepth + 1;
|
|
246
|
+
const currentDepth = workspaceQueue.getDepth(workspacePath);
|
|
247
|
+
const newDepth = workspaceQueue.incrementDepth(workspacePath);
|
|
258
248
|
if (currentDepth > 0) {
|
|
259
249
|
logger_1.logger.info(`[Queue:${projectLabel}] Enqueued (depth: ${newDepth}, channel: ${message.channelId})`);
|
|
260
250
|
await message.react('⏳').catch(() => { });
|
|
@@ -263,7 +253,7 @@ function createMessageCreateHandler(deps) {
|
|
|
263
253
|
logger_1.logger.info(`[Queue:${projectLabel}] Processing immediately (depth: ${newDepth}, channel: ${message.channelId})`);
|
|
264
254
|
}
|
|
265
255
|
const queueStartTime = Date.now();
|
|
266
|
-
await
|
|
256
|
+
await workspaceQueue.enqueue(workspacePath, async () => {
|
|
267
257
|
const waitMs = Date.now() - queueStartTime;
|
|
268
258
|
if (waitMs > 100) {
|
|
269
259
|
logger_1.logger.info(`[Queue:${projectLabel}] Task started after ${Math.round(waitMs / 1000)}s wait (channel: ${message.channelId})`);
|
|
@@ -349,9 +339,16 @@ function createMessageCreateHandler(deps) {
|
|
|
349
339
|
}
|
|
350
340
|
}
|
|
351
341
|
if (!activationResult.ok) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
342
|
+
const isDeleted = activationResult.error?.includes('Conversation not found in Past Conversations');
|
|
343
|
+
let replyText = `⚠️ Could not route this message to the bound session (${session.displayName}).\n*Reason: ${activationResult.error}*`;
|
|
344
|
+
if (isDeleted) {
|
|
345
|
+
deps.chatSessionRepo.deleteByChannelId(message.channelId);
|
|
346
|
+
replyText += `\n\n💡 **Tip**: This session appears to have been deleted in the IDE. I have unbound this channel so your next message will start a fresh chat. You can also type \`/new\` anytime.`;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
replyText += `\n\n💡 If this session is broken, type \`/new\` to force a new chat.`;
|
|
350
|
+
}
|
|
351
|
+
await message.reply(replyText).catch(() => { });
|
|
355
352
|
return;
|
|
356
353
|
}
|
|
357
354
|
}
|
|
@@ -427,8 +424,7 @@ function createMessageCreateHandler(deps) {
|
|
|
427
424
|
await message.reply(`Failed to connect to workspace: ${e.message}`);
|
|
428
425
|
}
|
|
429
426
|
finally {
|
|
430
|
-
const remainingDepth =
|
|
431
|
-
workspaceQueueDepths.set(workspacePath, remainingDepth);
|
|
427
|
+
const remainingDepth = workspaceQueue.decrementDepth(workspacePath);
|
|
432
428
|
if (remainingDepth > 0) {
|
|
433
429
|
logger_1.logger.info(`[Queue:${projectLabel}] Task done, ${remainingDepth} remaining`);
|
|
434
430
|
}
|