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.
@@ -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}`, components: [] };
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}` }).catch(() => { });
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: `✅ Skipped question`, components: [] };
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: `✅ Skipped question` }).catch(() => { });
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
- const DETECT_APPROVAL_SCRIPT = `(() => {
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' || node.classList.contains('cursor-pointer')) return;
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
- description = parentText.length > 2000 ? parentText.substring(0, 2000) + '...\\n(truncated)' : parentText;
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"], [class*="button"], [class*="action"]')).reverse();
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
- this.brainBasePath =
73
- brainBasePath ??
74
- path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
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 && content.includes(filterStr)) {
206
- belongsToWorkspace = true;
207
- }
208
- if (belongsToWorkspace) {
209
- if (content.includes(needle)) {
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
- else {
213
- const contentTokens = new Set(content.replace(/[^\p{L}\p{N}]+/gu, ' ').split(/\s+/));
214
- let currentScore = 0;
215
- for (const word of uniqueNeedleWords) {
216
- if (contentTokens.has(word))
217
- currentScore++;
218
- }
219
- if (currentScore > score)
220
- score = currentScore;
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
- // Find the first one that has artifacts
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
- // Guard against approval dialogs leaking into the output (they contain plan cards or review buttons)
212
- var dialogNode = node.closest('dialog, [role="dialog"], form');
213
- if (dialogNode && dialogNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
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
  });