lazy-gravity 0.10.0 → 0.11.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/dist/bin/commands/doctor.js +45 -7
- package/dist/bot/index.js +191 -22
- package/dist/commands/chatCommandHandler.js +11 -41
- package/dist/events/interactionCreateHandler.js +158 -27
- package/dist/events/messageCreateHandler.js +14 -4
- 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/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/utils/fileOpenCache.js +2 -6
- package/dist/utils/questionActionUtils.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.execute = execute;
|
|
4
|
+
const i18n_1 = require("../utils/i18n");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
async function execute(interaction, projectName, pool, accountName) {
|
|
7
|
+
try {
|
|
8
|
+
if (!projectName) {
|
|
9
|
+
await interaction.reply({ text: (0, i18n_1.t)('Project name not found.'), ephemeral: true });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const cdp = pool.getConnected(projectName, accountName);
|
|
13
|
+
if (!cdp) {
|
|
14
|
+
await interaction.reply({ text: (0, i18n_1.t)('Not connected to workspace.'), ephemeral: true });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const comment = interaction.fields.get('comment')?.trim();
|
|
18
|
+
if (!comment) {
|
|
19
|
+
await interaction.reply({ text: (0, i18n_1.t)('Comment cannot be empty.'), ephemeral: true });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await interaction.deferUpdate();
|
|
23
|
+
// Inject the comment into the chat
|
|
24
|
+
const result = await cdp.injectMessage(comment);
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
await interaction.followUp({ text: (0, i18n_1.t)('Failed to submit comment: {{error}}', { error: result.error }), ephemeral: true });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await interaction.followUp({ text: (0, i18n_1.t)('Comment submitted to the plan successfully.'), ephemeral: true });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger_1.logger.error('[PlanningModalSubmit] Error:', error);
|
|
33
|
+
try {
|
|
34
|
+
await interaction.followUp({ text: (0, i18n_1.t)('An error occurred while submitting your comment.'), ephemeral: true });
|
|
35
|
+
}
|
|
36
|
+
catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -13,16 +13,16 @@ function createQuestionSelectAction(deps) {
|
|
|
13
13
|
async execute(interaction, values) {
|
|
14
14
|
const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
|
|
15
15
|
if (!parsed)
|
|
16
|
-
return;
|
|
16
|
+
return false;
|
|
17
17
|
const selectedOption = parseInt(values[0], 10);
|
|
18
18
|
if (isNaN(selectedOption))
|
|
19
|
-
return;
|
|
19
|
+
return false;
|
|
20
20
|
const channelId = parsed.channelId;
|
|
21
21
|
if (channelId && channelId !== interaction.channel.id) {
|
|
22
22
|
await interaction
|
|
23
23
|
.reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
|
|
24
24
|
.catch(() => { });
|
|
25
|
-
return;
|
|
25
|
+
return false;
|
|
26
26
|
}
|
|
27
27
|
const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
|
|
28
28
|
logger_1.logger.debug(`[QuestionSelectAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
|
|
@@ -34,7 +34,7 @@ function createQuestionSelectAction(deps) {
|
|
|
34
34
|
await interaction
|
|
35
35
|
.reply({ text: 'Question detector not found.', ephemeral: true })
|
|
36
36
|
.catch(() => { });
|
|
37
|
-
return;
|
|
37
|
+
return false;
|
|
38
38
|
}
|
|
39
39
|
await interaction.deferUpdate().catch(() => { });
|
|
40
40
|
let success = false;
|
|
@@ -47,16 +47,16 @@ function createQuestionSelectAction(deps) {
|
|
|
47
47
|
await interaction
|
|
48
48
|
.followUp({ text: `Failed to answer question: ${msg}`, ephemeral: true })
|
|
49
49
|
.catch(() => { });
|
|
50
|
-
return;
|
|
50
|
+
return false;
|
|
51
51
|
}
|
|
52
52
|
if (success) {
|
|
53
|
-
const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}
|
|
53
|
+
const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...`, components: [] };
|
|
54
54
|
try {
|
|
55
55
|
await interaction.editReply(updatePayload);
|
|
56
56
|
}
|
|
57
57
|
catch (editErr) {
|
|
58
58
|
logger_1.logger.warn('[QuestionSelectAction] editReply failed, sending followUp:', editErr);
|
|
59
|
-
await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}
|
|
59
|
+
await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...` }).catch(() => { });
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
@@ -64,6 +64,7 @@ function createQuestionSelectAction(deps) {
|
|
|
64
64
|
.followUp({ text: 'Failed to answer question (it may have been resolved already).', ephemeral: true })
|
|
65
65
|
.catch(() => { });
|
|
66
66
|
}
|
|
67
|
+
return success;
|
|
67
68
|
},
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -14,13 +14,13 @@ function createQuestionSkipAction(deps) {
|
|
|
14
14
|
async execute(interaction, params) {
|
|
15
15
|
const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
|
|
16
16
|
if (!parsed)
|
|
17
|
-
return;
|
|
17
|
+
return false;
|
|
18
18
|
const channelId = parsed.channelId;
|
|
19
19
|
if (channelId && channelId !== interaction.channel.id) {
|
|
20
20
|
await interaction
|
|
21
21
|
.reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
|
|
22
22
|
.catch(() => { });
|
|
23
|
-
return;
|
|
23
|
+
return false;
|
|
24
24
|
}
|
|
25
25
|
const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
|
|
26
26
|
logger_1.logger.debug(`[QuestionSkipAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
|
|
@@ -32,7 +32,7 @@ function createQuestionSkipAction(deps) {
|
|
|
32
32
|
await interaction
|
|
33
33
|
.reply({ text: 'Question detector not found.', ephemeral: true })
|
|
34
34
|
.catch(() => { });
|
|
35
|
-
return;
|
|
35
|
+
return false;
|
|
36
36
|
}
|
|
37
37
|
await interaction.deferUpdate().catch(() => { });
|
|
38
38
|
let success = false;
|
|
@@ -45,16 +45,16 @@ function createQuestionSkipAction(deps) {
|
|
|
45
45
|
await interaction
|
|
46
46
|
.followUp({ text: `Failed to skip question: ${msg}`, ephemeral: true })
|
|
47
47
|
.catch(() => { });
|
|
48
|
-
return;
|
|
48
|
+
return false;
|
|
49
49
|
}
|
|
50
50
|
if (success) {
|
|
51
|
-
const updatePayload = { text:
|
|
51
|
+
const updatePayload = { text: '✅ Skipped question.\n\n⏳ IDE is working on the response...', components: [] };
|
|
52
52
|
try {
|
|
53
53
|
await interaction.editReply(updatePayload);
|
|
54
54
|
}
|
|
55
55
|
catch (editErr) {
|
|
56
56
|
logger_1.logger.warn('[QuestionSkipAction] editReply failed, sending followUp:', editErr);
|
|
57
|
-
await interaction.followUp({ text:
|
|
57
|
+
await interaction.followUp({ text: '✅ Skipped question.\n\n⏳ IDE is working on the response...' }).catch(() => { });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
@@ -62,6 +62,7 @@ function createQuestionSkipAction(deps) {
|
|
|
62
62
|
.followUp({ text: 'Failed to skip question (it may have been resolved already).', ephemeral: true })
|
|
63
63
|
.catch(() => { });
|
|
64
64
|
}
|
|
65
|
+
return success;
|
|
65
66
|
},
|
|
66
67
|
};
|
|
67
68
|
}
|
|
@@ -15,6 +15,7 @@ exports.wrapDiscordMessage = wrapDiscordMessage;
|
|
|
15
15
|
exports.wrapDiscordButton = wrapDiscordButton;
|
|
16
16
|
exports.wrapDiscordSelect = wrapDiscordSelect;
|
|
17
17
|
exports.wrapDiscordCommand = wrapDiscordCommand;
|
|
18
|
+
exports.wrapDiscordModalSubmit = wrapDiscordModalSubmit;
|
|
18
19
|
const discord_js_1 = require("discord.js");
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Style mapping
|
|
@@ -107,6 +108,32 @@ function toDiscordComponents(rows) {
|
|
|
107
108
|
return actionRow;
|
|
108
109
|
});
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Convert a platform ModalDef to a discord.js ModalBuilder.
|
|
113
|
+
*/
|
|
114
|
+
function toDiscordModal(modal) {
|
|
115
|
+
const builder = new discord_js_1.ModalBuilder()
|
|
116
|
+
.setCustomId(modal.customId)
|
|
117
|
+
.setTitle(modal.title);
|
|
118
|
+
for (const row of modal.components) {
|
|
119
|
+
const actionRow = new discord_js_1.ActionRowBuilder();
|
|
120
|
+
for (const comp of row.components) {
|
|
121
|
+
const input = new discord_js_1.TextInputBuilder()
|
|
122
|
+
.setCustomId(comp.customId)
|
|
123
|
+
.setLabel(comp.label)
|
|
124
|
+
.setStyle(comp.style === 'paragraph' ? discord_js_1.TextInputStyle.Paragraph : discord_js_1.TextInputStyle.Short);
|
|
125
|
+
if (comp.placeholder !== undefined) {
|
|
126
|
+
input.setPlaceholder(comp.placeholder);
|
|
127
|
+
}
|
|
128
|
+
if (comp.required !== undefined) {
|
|
129
|
+
input.setRequired(comp.required);
|
|
130
|
+
}
|
|
131
|
+
actionRow.addComponents(input);
|
|
132
|
+
}
|
|
133
|
+
builder.addComponents(actionRow);
|
|
134
|
+
}
|
|
135
|
+
return builder;
|
|
136
|
+
}
|
|
110
137
|
/**
|
|
111
138
|
* Convert a platform MessagePayload to discord.js message send/reply options.
|
|
112
139
|
*
|
|
@@ -256,6 +283,9 @@ function wrapDiscordButton(interaction) {
|
|
|
256
283
|
const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
257
284
|
return wrapDiscordSentMessage(sent);
|
|
258
285
|
},
|
|
286
|
+
async showModal(modal) {
|
|
287
|
+
await interaction.showModal(toDiscordModal(modal));
|
|
288
|
+
},
|
|
259
289
|
};
|
|
260
290
|
}
|
|
261
291
|
/** Wrap a discord.js StringSelectMenuInteraction as a PlatformSelectInteraction. */
|
|
@@ -329,3 +359,49 @@ function wrapDiscordCommand(interaction) {
|
|
|
329
359
|
},
|
|
330
360
|
};
|
|
331
361
|
}
|
|
362
|
+
/** Wrap a discord.js ModalSubmitInteraction as a PlatformModalSubmitInteraction. */
|
|
363
|
+
function wrapDiscordModalSubmit(interaction) {
|
|
364
|
+
const user = wrapDiscordUser(interaction.user);
|
|
365
|
+
const channel = interaction.channel
|
|
366
|
+
? wrapDiscordChannel(interaction.channel)
|
|
367
|
+
: buildFallbackChannel(interaction.channelId ?? 'unknown');
|
|
368
|
+
const fields = new Map();
|
|
369
|
+
for (const [key, field] of interaction.fields.fields.entries()) {
|
|
370
|
+
fields.set(key, interaction.fields.getTextInputValue(key));
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
id: interaction.id,
|
|
374
|
+
platform: 'discord',
|
|
375
|
+
customId: interaction.customId,
|
|
376
|
+
user,
|
|
377
|
+
channel,
|
|
378
|
+
messageId: interaction.message?.id ?? null,
|
|
379
|
+
fields,
|
|
380
|
+
async deferUpdate() {
|
|
381
|
+
if (interaction.isFromMessage()) {
|
|
382
|
+
await interaction.deferUpdate();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
await interaction.deferReply({ ephemeral: true });
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
async reply(payload) {
|
|
389
|
+
await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
390
|
+
},
|
|
391
|
+
async update(payload) {
|
|
392
|
+
if (interaction.isFromMessage()) {
|
|
393
|
+
await interaction.update(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
async editReply(payload) {
|
|
400
|
+
await interaction.editReply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
401
|
+
},
|
|
402
|
+
async followUp(payload) {
|
|
403
|
+
const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
|
|
404
|
+
return wrapDiscordSentMessage(sent);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ApprovalDetector = void 0;
|
|
3
|
+
exports.ApprovalDetector = exports.DETECT_APPROVAL_SCRIPT = void 0;
|
|
4
4
|
exports.buildClickScript = buildClickScript;
|
|
5
5
|
const logger_1 = require("../utils/logger");
|
|
6
6
|
const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
|
|
@@ -9,7 +9,7 @@ const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
|
|
|
9
9
|
*
|
|
10
10
|
* Detects allow/deny button pairs and extracts descriptions with fallbacks.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
exports.DETECT_APPROVAL_SCRIPT = `(() => {
|
|
13
13
|
const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', 'yes, allow this time', '今回のみ許可', '1回のみ許可', '一度許可'];
|
|
14
14
|
const ALWAYS_ALLOW_PATTERNS = [
|
|
15
15
|
'allow this conversation',
|
|
@@ -25,7 +25,21 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
25
25
|
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
26
26
|
|
|
27
27
|
const allButtons = Array.from(document.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
28
|
-
.filter(btn => btn.offsetParent !== null)
|
|
28
|
+
.filter(btn => btn.offsetParent !== null)
|
|
29
|
+
.reverse();
|
|
30
|
+
|
|
31
|
+
const STOP_PATTERNS_GEN = [/^stop$/, /^stop generating$/, /^stop response$/, /^停止$/, /^生成を停止$/, /^応答を停止$/];
|
|
32
|
+
const isGenerating = allButtons.some(btn => {
|
|
33
|
+
const tooltipId = btn.getAttribute('data-tooltip-id');
|
|
34
|
+
if (tooltipId === 'input-send-button-cancel-tooltip') return true;
|
|
35
|
+
const labels = [btn.textContent || '', btn.getAttribute('aria-label') || '', btn.getAttribute('title') || ''];
|
|
36
|
+
return labels.some(val => {
|
|
37
|
+
const normalized = (val || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
38
|
+
return normalized && STOP_PATTERNS_GEN.some(re => re.test(normalized));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (isGenerating) return null; // Wait for generation to finish!
|
|
29
43
|
|
|
30
44
|
let approveBtn = allButtons.find(btn => {
|
|
31
45
|
const t = normalize(btn.textContent || '');
|
|
@@ -48,7 +62,8 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
48
62
|
|| document.body;
|
|
49
63
|
|
|
50
64
|
const containerButtons = Array.from(container.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
51
|
-
.filter(btn => btn.offsetParent !== null)
|
|
65
|
+
.filter(btn => btn.offsetParent !== null)
|
|
66
|
+
.reverse();
|
|
52
67
|
|
|
53
68
|
const denyBtn = containerButtons.find(btn => {
|
|
54
69
|
const t = normalize(btn.textContent || '');
|
|
@@ -62,9 +77,9 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
62
77
|
return ALWAYS_ALLOW_PATTERNS.some(p => t.includes(p));
|
|
63
78
|
}) || null;
|
|
64
79
|
|
|
65
|
-
const approveText = (approveBtn.textContent || '').trim();
|
|
66
|
-
const alwaysAllowText = alwaysAllowBtn ? (alwaysAllowBtn.textContent || '').trim() : '';
|
|
67
|
-
const denyText = (denyBtn.textContent || '').trim();
|
|
80
|
+
const approveText = (approveBtn.innerText || approveBtn.textContent || '').trim();
|
|
81
|
+
const alwaysAllowText = alwaysAllowBtn ? (alwaysAllowBtn.innerText || alwaysAllowBtn.textContent || '').trim() : '';
|
|
82
|
+
const denyText = (denyBtn.innerText || denyBtn.textContent || '').trim();
|
|
68
83
|
|
|
69
84
|
// Description extraction (multiple fallbacks)
|
|
70
85
|
let description = '';
|
|
@@ -95,6 +110,7 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
95
110
|
}
|
|
96
111
|
if (!modal) {
|
|
97
112
|
modal = approveBtn.parentElement?.parentElement?.parentElement || approveBtn.parentElement?.parentElement;
|
|
113
|
+
if (modal === document.body || modal?.id === 'root') modal = null;
|
|
98
114
|
}
|
|
99
115
|
}
|
|
100
116
|
|
|
@@ -103,8 +119,11 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
103
119
|
const walk = (node) => {
|
|
104
120
|
if (node.nodeType === 1) {
|
|
105
121
|
// Skip buttons entirely
|
|
106
|
-
if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button'
|
|
122
|
+
if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button') return;
|
|
107
123
|
|
|
124
|
+
// Skip menu bars and sidebars
|
|
125
|
+
if (node.tagName === 'NAV' || node.getAttribute('role') === 'menubar' || node.closest('nav') || node.closest('.monaco-menu') || node.closest('.sidebar') || node.classList.contains('sidebar')) return;
|
|
126
|
+
|
|
108
127
|
const display = window.getComputedStyle(node).display;
|
|
109
128
|
if (display === 'none') return;
|
|
110
129
|
|
|
@@ -121,7 +140,11 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
121
140
|
|
|
122
141
|
const parentText = parts.join(' ').replace(/\\n\\s*/g, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
123
142
|
if (parentText.length > 5) {
|
|
124
|
-
|
|
143
|
+
if (parentText.length > 800 || parentText.includes('F ile\\nE dit\\nS election')) {
|
|
144
|
+
description = 'Code changes require your approval.';
|
|
145
|
+
} else {
|
|
146
|
+
description = parentText;
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
150
|
}
|
|
@@ -149,7 +172,7 @@ const EXPAND_ALWAYS_ALLOW_MENU_SCRIPT = `(() => {
|
|
|
149
172
|
];
|
|
150
173
|
|
|
151
174
|
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
152
|
-
const visibleButtons = Array.from(document.querySelectorAll('button'))
|
|
175
|
+
const visibleButtons = Array.from(document.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
153
176
|
.filter(btn => btn.offsetParent !== null);
|
|
154
177
|
|
|
155
178
|
const directAlways = visibleButtons.find(btn => {
|
|
@@ -169,7 +192,7 @@ const EXPAND_ALWAYS_ALLOW_MENU_SCRIPT = `(() => {
|
|
|
169
192
|
|| allowOnceBtn.parentElement
|
|
170
193
|
|| document.body;
|
|
171
194
|
|
|
172
|
-
const containerButtons = Array.from(container.querySelectorAll('button'))
|
|
195
|
+
const containerButtons = Array.from(container.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
173
196
|
.filter(btn => btn.offsetParent !== null);
|
|
174
197
|
|
|
175
198
|
const toggleBtn = containerButtons.find(btn => {
|
|
@@ -218,12 +241,18 @@ function buildClickScript(buttonText) {
|
|
|
218
241
|
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
219
242
|
const text = ${safeText};
|
|
220
243
|
const wanted = normalize(text);
|
|
221
|
-
const allButtons = Array.from(document.querySelectorAll('button, [role="button"], a, [class*="btn"],
|
|
244
|
+
const allButtons = Array.from(document.querySelectorAll('button, [role="button"], a.action-btn, a[class*="btn"], span.cursor-pointer, div.cursor-pointer')).reverse();
|
|
222
245
|
const target = allButtons.find(btn => {
|
|
223
246
|
const style = window.getComputedStyle(btn);
|
|
224
247
|
if (style.display === 'none' || style.visibility === 'hidden' || btn.disabled) return false;
|
|
225
|
-
const buttonText = normalize(btn.textContent || '');
|
|
248
|
+
const buttonText = normalize(btn.innerText || btn.textContent || '');
|
|
226
249
|
const ariaLabel = normalize(btn.getAttribute('aria-label') || '');
|
|
250
|
+
|
|
251
|
+
const isShort = wanted.length < 5;
|
|
252
|
+
if (isShort) {
|
|
253
|
+
return buttonText === wanted || ariaLabel === wanted;
|
|
254
|
+
}
|
|
255
|
+
|
|
227
256
|
return buttonText === wanted ||
|
|
228
257
|
ariaLabel === wanted ||
|
|
229
258
|
(buttonText.includes(wanted) && buttonText.length < wanted.length + 10) ||
|
|
@@ -317,7 +346,7 @@ class ApprovalDetector {
|
|
|
317
346
|
try {
|
|
318
347
|
const contextId = this.cdpService.getPrimaryContextId();
|
|
319
348
|
const callParams = {
|
|
320
|
-
expression: DETECT_APPROVAL_SCRIPT,
|
|
349
|
+
expression: exports.DETECT_APPROVAL_SCRIPT,
|
|
321
350
|
returnByValue: true,
|
|
322
351
|
awaitPromise: false,
|
|
323
352
|
};
|
|
@@ -69,9 +69,17 @@ const COMMON_WORDS = new Set(['the', 'and', 'for', 'with', 'from', 'this', 'that
|
|
|
69
69
|
class ArtifactService {
|
|
70
70
|
brainBasePath;
|
|
71
71
|
constructor(brainBasePath) {
|
|
72
|
-
|
|
73
|
-
brainBasePath
|
|
74
|
-
|
|
72
|
+
if (brainBasePath) {
|
|
73
|
+
this.brainBasePath = brainBasePath;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const idePath = path.join(os.homedir(), '.gemini', 'antigravity-ide', 'brain');
|
|
77
|
+
const defaultPath = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
|
78
|
+
this.brainBasePath = fs.existsSync(idePath) ? idePath : defaultPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getBrainBasePath() {
|
|
82
|
+
return this.brainBasePath;
|
|
75
83
|
}
|
|
76
84
|
/**
|
|
77
85
|
* List all conversations in the brain directory (UUIDs).
|
|
@@ -202,23 +210,25 @@ class ArtifactService {
|
|
|
202
210
|
const buf = Buffer.alloc(readSize);
|
|
203
211
|
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
204
212
|
const content = buf.slice(0, bytesRead).toString('utf-8').toLowerCase();
|
|
205
|
-
if (filterStr
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
exactMatch = true;
|
|
213
|
+
if (filterStr) {
|
|
214
|
+
const escapedFilter = filterStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
const regex = new RegExp(`(?:file:\\/\\/\\/[^"\\)]*?\\/|[a-zA-Z]:[\\\\/](?!.*(?:\\.gemini|brain|antigravity))[^"\\)]*?[\\\\/])${escapedFilter}\\b`, 'i');
|
|
216
|
+
if (regex.test(content)) {
|
|
217
|
+
belongsToWorkspace = true;
|
|
211
218
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
}
|
|
220
|
+
if (content.includes(needle)) {
|
|
221
|
+
exactMatch = true;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const contentTokens = new Set(content.replace(/[^\p{L}\p{N}]+/gu, ' ').split(/\s+/));
|
|
225
|
+
let currentScore = 0;
|
|
226
|
+
for (const word of uniqueNeedleWords) {
|
|
227
|
+
if (contentTokens.has(word))
|
|
228
|
+
currentScore++;
|
|
221
229
|
}
|
|
230
|
+
if (currentScore > score)
|
|
231
|
+
score = currentScore;
|
|
222
232
|
}
|
|
223
233
|
}
|
|
224
234
|
finally {
|
|
@@ -251,10 +261,11 @@ class ArtifactService {
|
|
|
251
261
|
* that has at least one artifact. Falls back to the most recent conversation
|
|
252
262
|
* overall if none have artifacts.
|
|
253
263
|
*/
|
|
254
|
-
getLatestConversationWithArtifacts() {
|
|
264
|
+
getLatestConversationWithArtifacts(workspaceFilter) {
|
|
255
265
|
const ids = this.listConversationIds();
|
|
256
266
|
if (ids.length === 0)
|
|
257
267
|
return null;
|
|
268
|
+
const filterStr = workspaceFilter ? workspaceFilter.toLowerCase() : null;
|
|
258
269
|
// Sort by directory mtime descending
|
|
259
270
|
const sorted = ids
|
|
260
271
|
.map((id) => {
|
|
@@ -267,12 +278,41 @@ class ArtifactService {
|
|
|
267
278
|
}
|
|
268
279
|
})
|
|
269
280
|
.sort((a, b) => b.mtime - a.mtime);
|
|
270
|
-
|
|
281
|
+
if (filterStr) {
|
|
282
|
+
const escapedFilter = filterStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
283
|
+
const workspaceIds = sorted.filter(({ id }) => {
|
|
284
|
+
let belongs = false;
|
|
285
|
+
const regex = new RegExp(`(?:file:\\/\\/\\/[^"\\)]*?\\/|[a-zA-Z]:[\\\\/](?!.*(?:\\.gemini|brain|antigravity))[^"\\)]*?[\\\\/])${escapedFilter}\\b`, 'i');
|
|
286
|
+
const checkFile = (filePath) => {
|
|
287
|
+
try {
|
|
288
|
+
if (fs.existsSync(filePath)) {
|
|
289
|
+
const content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
|
|
290
|
+
if (regex.test(content)) {
|
|
291
|
+
belongs = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch { /* ignore */ }
|
|
296
|
+
};
|
|
297
|
+
checkFile(path.join(this.brainBasePath, id, '.system_generated', 'logs', 'transcript.jsonl'));
|
|
298
|
+
if (!belongs) {
|
|
299
|
+
checkFile(path.join(this.brainBasePath, id, 'implementation_plan.md'));
|
|
300
|
+
}
|
|
301
|
+
return belongs;
|
|
302
|
+
});
|
|
303
|
+
if (workspaceIds.length === 0)
|
|
304
|
+
return null;
|
|
305
|
+
const latestId = workspaceIds[0].id;
|
|
306
|
+
if (this.listArtifacts(latestId).length > 0) {
|
|
307
|
+
return latestId;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
// Original fallback behavior for no filter
|
|
271
312
|
for (const { id } of sorted) {
|
|
272
313
|
if (this.listArtifacts(id).length > 0)
|
|
273
314
|
return id;
|
|
274
315
|
}
|
|
275
|
-
// Nothing has artifacts — return most recent anyway (caller handles empty list)
|
|
276
316
|
return sorted[0]?.id ?? null;
|
|
277
317
|
}
|
|
278
318
|
/**
|
|
@@ -208,9 +208,10 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
208
208
|
if (node.closest('details')) return true;
|
|
209
209
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
210
210
|
if (node.closest('.notify-user-container')) return true;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
if (node.closest('dialog, [role="dialog"]')) return true;
|
|
212
|
+
// Guard against other approval forms leaking into the output
|
|
213
|
+
var formNode = node.closest('form');
|
|
214
|
+
if (formNode && formNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
|
|
214
215
|
return true;
|
|
215
216
|
}
|
|
216
217
|
|
|
@@ -532,6 +533,25 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
532
533
|
});
|
|
533
534
|
}
|
|
534
535
|
}
|
|
536
|
+
// 4f. Artifact Cards
|
|
537
|
+
var artifactCards = artifactScope.querySelectorAll('[data-testid="artifact-card"], [class*="artifact-card"]');
|
|
538
|
+
for (var aci = 0; aci < artifactCards.length; aci++) {
|
|
539
|
+
var card = artifactCards[aci];
|
|
540
|
+
var titleEl = card.querySelector('.title, [class*="title"], h3, h4, span');
|
|
541
|
+
if (titleEl) {
|
|
542
|
+
var titleText = (titleEl.textContent || '').trim();
|
|
543
|
+
if (titleText) {
|
|
544
|
+
segments.push({
|
|
545
|
+
kind: 'citation',
|
|
546
|
+
text: titleText + '.md',
|
|
547
|
+
role: 'assistant',
|
|
548
|
+
messageIndex: 0,
|
|
549
|
+
domPath: 'artifact-card:nth(' + aci + ')'
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
535
555
|
|
|
536
556
|
if (!bodyFound && segments.length === 0) return null;
|
|
537
557
|
|
|
@@ -56,7 +56,7 @@ function buildSessionRouteKey(projectName, sessionTitle) {
|
|
|
56
56
|
return `${projectName}::${normalizeSessionTitle(sessionTitle)}`;
|
|
57
57
|
}
|
|
58
58
|
const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
|
|
59
|
-
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
59
|
+
const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
|
|
60
60
|
if (!panel) return '';
|
|
61
61
|
const header = panel.querySelector('div[class*="border-b"]');
|
|
62
62
|
if (!header) return '';
|
|
@@ -337,6 +337,8 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
|
|
|
337
337
|
},
|
|
338
338
|
onApprovalRequired: async (info) => {
|
|
339
339
|
logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
|
|
340
|
+
// Emit approval event for bot text updating
|
|
341
|
+
cdp.emit('approval_required', info);
|
|
340
342
|
const currentChatTitle = await getCurrentChatTitle(cdp);
|
|
341
343
|
const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
|
|
342
344
|
const targetChannelId = targetChannel ? targetChannel.id : '';
|
|
@@ -365,6 +367,24 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
|
|
|
365
367
|
extraFields.push({ name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText, inline: true });
|
|
366
368
|
}
|
|
367
369
|
extraFields.push({ name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true });
|
|
370
|
+
// Resolve artifact review buttons if they exist
|
|
371
|
+
let walkthroughCustomId;
|
|
372
|
+
let taskCustomId;
|
|
373
|
+
if (bridge.chatSessionRepo && bridge.artifactService) {
|
|
374
|
+
const session = bridge.chatSessionRepo.findByChannelId(targetChannelId);
|
|
375
|
+
const activeConversationId = session?.conversationId;
|
|
376
|
+
if (activeConversationId) {
|
|
377
|
+
const artifacts = bridge.artifactService.listArtifacts(activeConversationId);
|
|
378
|
+
const walkthrough = artifacts.find((art) => art.filename.toLowerCase() === 'walkthrough.md');
|
|
379
|
+
if (walkthrough) {
|
|
380
|
+
walkthroughCustomId = `file_open:art:${activeConversationId}:walkthrough.md`;
|
|
381
|
+
}
|
|
382
|
+
const task = artifacts.find((art) => art.filename.toLowerCase() === 'task.md');
|
|
383
|
+
if (task) {
|
|
384
|
+
taskCustomId = `file_open:art:${activeConversationId}:task.md`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
368
388
|
const payload = (0, notificationSender_1.buildApprovalNotification)({
|
|
369
389
|
title: (0, i18n_1.t)('Approval Required'),
|
|
370
390
|
description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
|
|
@@ -373,6 +393,8 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
|
|
|
373
393
|
extraFields,
|
|
374
394
|
hasAlwaysAllow: !!info.alwaysAllowText,
|
|
375
395
|
alwaysAllowText: info.alwaysAllowText,
|
|
396
|
+
walkthroughCustomId,
|
|
397
|
+
taskCustomId,
|
|
376
398
|
});
|
|
377
399
|
if (lastNotification) {
|
|
378
400
|
lastNotification.payload = payload;
|
|
@@ -503,12 +525,23 @@ function ensureErrorPopupDetector(bridge, cdp, projectName, accountName = 'defau
|
|
|
503
525
|
{ name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
|
|
504
526
|
],
|
|
505
527
|
});
|
|
528
|
+
if (lastNotification && detector.lastDetectedKey && lastNotification.key === detector.lastDetectedKey) {
|
|
529
|
+
// Cooldown triggered for the same popup; edit existing message instead of sending new
|
|
530
|
+
const edited = await lastNotification.sent.edit(payload).catch((err) => {
|
|
531
|
+
logger_1.logger.error(err);
|
|
532
|
+
return null;
|
|
533
|
+
});
|
|
534
|
+
if (edited) {
|
|
535
|
+
lastNotification.payload = payload;
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
506
539
|
const sent = await targetChannel.send(payload).catch((err) => {
|
|
507
540
|
logger_1.logger.error(err);
|
|
508
541
|
return null;
|
|
509
542
|
});
|
|
510
543
|
if (sent) {
|
|
511
|
-
lastNotification = { sent, payload };
|
|
544
|
+
lastNotification = { sent, payload, key: detector.lastDetectedKey };
|
|
512
545
|
}
|
|
513
546
|
},
|
|
514
547
|
});
|