lazy-gravity 0.8.1 → 0.9.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 +3 -2
- package/dist/bot/index.js +29 -0
- package/dist/commands/registerSlashCommands.js +6 -0
- package/dist/commands/workspaceCommandHandler.js +7 -1
- package/dist/services/antigravityLauncher.js +130 -0
- package/dist/services/assistantDomExtractor.js +53 -6
- package/dist/services/cdpBridgeManager.js +12 -1
- package/dist/services/cdpService.js +28 -13
- package/dist/services/chatSessionService.js +53 -8
- package/dist/services/responseMonitor.js +41 -9
- package/dist/services/userMessageDetector.js +32 -31
- package/dist/utils/configLoader.js +3 -0
- package/dist/utils/pathUtils.js +3 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="https://img.shields.io/
|
|
6
|
+
<img src="https://img.shields.io/npm/v/lazy-gravity?style=flat-square&color=blue" alt="Version" />
|
|
7
7
|
<img src="https://img.shields.io/badge/Antigravity-1.19.5-ff6b35?style=flat-square" alt="Antigravity" />
|
|
8
8
|
<img src="https://img.shields.io/badge/node-18.x+-brightgreen?style=flat-square&logo=node.js" alt="Node.js" />
|
|
9
9
|
<img src="https://img.shields.io/badge/discord.js-14.x-5865F2?style=flat-square&logo=discord&logoColor=white" alt="discord.js" />
|
|
@@ -230,7 +230,8 @@ Double-click **`start_antigravity_mac.command`** in the repo root.
|
|
|
230
230
|
#### Windows
|
|
231
231
|
Double-click **`start_antigravity_win.bat`** in the repo root.
|
|
232
232
|
|
|
233
|
-
- **If it doesn't launch**:
|
|
233
|
+
- **If it doesn't launch**: verify that Antigravity IDE is installed at `"%LOCALAPPDATA%\Programs\Antigravity IDE\Antigravity IDE.exe"`. If it is installed elsewhere, right-click the file and update the executable path.
|
|
234
|
+
- **Upgrading to Antigravity 2.0?** The Windows executable was renamed to `Antigravity IDE.exe`. If you are still using the older `Antigravity.exe` installation, auto-launch will not find it. Please update Antigravity, or set the `ANTIGRAVITY_PATH` override in your `.env` file.
|
|
234
235
|
|
|
235
236
|
#### Linux
|
|
236
237
|
On Linux (especially when using AppImages), the `antigravity` command might not be globally available.
|
package/dist/bot/index.js
CHANGED
|
@@ -873,6 +873,18 @@ const startBot = async (cliLogLevel) => {
|
|
|
873
873
|
await cdp.disconnect().catch(() => { });
|
|
874
874
|
}
|
|
875
875
|
await bridge.pool.getOrConnect(workspacePath, { name: selectedAccount });
|
|
876
|
+
}, async ({ channelId, userId }) => {
|
|
877
|
+
const accountName = (0, accountUtils_1.resolveScopedAccountName)({
|
|
878
|
+
channelId,
|
|
879
|
+
userId,
|
|
880
|
+
sessionAccountName: chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null,
|
|
881
|
+
parentChannelId: null,
|
|
882
|
+
selectedAccountByChannel: bridge.selectedAccountByChannel,
|
|
883
|
+
channelPrefRepo,
|
|
884
|
+
accountPrefRepo,
|
|
885
|
+
accounts: config.antigravityAccounts,
|
|
886
|
+
});
|
|
887
|
+
await (0, antigravityLauncher_1.startAntigravity)(accountPorts[accountName] ?? 9222);
|
|
876
888
|
});
|
|
877
889
|
const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool, (channelId, userId) => (0, accountUtils_1.resolveScopedAccountName)({
|
|
878
890
|
channelId,
|
|
@@ -1759,6 +1771,23 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
1759
1771
|
}
|
|
1760
1772
|
break;
|
|
1761
1773
|
}
|
|
1774
|
+
case 'shutdown': {
|
|
1775
|
+
try {
|
|
1776
|
+
bridge.pool.disconnectAll();
|
|
1777
|
+
const ports = [...new Set(antigravityAccounts.map((account) => account.cdpPort))];
|
|
1778
|
+
const results = await Promise.all(ports.map((port) => (0, antigravityLauncher_1.stopAntigravity)(port)));
|
|
1779
|
+
const stopped = results.some((result) => result === 'stopped');
|
|
1780
|
+
await interaction.editReply({
|
|
1781
|
+
content: stopped
|
|
1782
|
+
? '✅ Antigravity IDE shut down. Use `/project list` to start it again.'
|
|
1783
|
+
: 'ℹ️ Antigravity IDE is already stopped. Use `/project list` to start it.',
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
catch (e) {
|
|
1787
|
+
await interaction.editReply({ content: `❌ Failed to shut down Antigravity IDE: ${e.message}` });
|
|
1788
|
+
}
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1762
1791
|
case 'project': {
|
|
1763
1792
|
const wsSub = interaction.options.getSubcommand(false);
|
|
1764
1793
|
if (wsSub === 'create') {
|
|
@@ -50,6 +50,11 @@ const templateCommand = new discord_js_1.SlashCommandBuilder()
|
|
|
50
50
|
const stopCommand = new discord_js_1.SlashCommandBuilder()
|
|
51
51
|
.setName('stop')
|
|
52
52
|
.setDescription((0, i18n_1.t)('Interrupt active LLM generation'));
|
|
53
|
+
/** /shutdown command definition */
|
|
54
|
+
const shutdownCommand = new discord_js_1.SlashCommandBuilder()
|
|
55
|
+
.setName('shutdown')
|
|
56
|
+
.setDescription((0, i18n_1.t)('Shut down Antigravity IDE'))
|
|
57
|
+
.setDefaultMemberPermissions(discord_js_1.PermissionFlagsBits.Administrator);
|
|
53
58
|
/** /screenshot command definition */
|
|
54
59
|
const screenshotCommand = new discord_js_1.SlashCommandBuilder()
|
|
55
60
|
.setName('screenshot')
|
|
@@ -159,6 +164,7 @@ exports.slashCommands = [
|
|
|
159
164
|
modelCommand,
|
|
160
165
|
templateCommand,
|
|
161
166
|
stopCommand,
|
|
167
|
+
shutdownCommand,
|
|
162
168
|
screenshotCommand,
|
|
163
169
|
statusCommand,
|
|
164
170
|
autoAcceptCommand,
|
|
@@ -23,6 +23,7 @@ class WorkspaceCommandHandler {
|
|
|
23
23
|
workspaceService;
|
|
24
24
|
channelManager;
|
|
25
25
|
onSessionChannelCreated;
|
|
26
|
+
ensureIdeRunning;
|
|
26
27
|
processingWorkspaces = new Set();
|
|
27
28
|
/**
|
|
28
29
|
* Filters out stale bindings where the Discord channel no longer exists.
|
|
@@ -59,17 +60,22 @@ class WorkspaceCommandHandler {
|
|
|
59
60
|
}
|
|
60
61
|
return validBindings;
|
|
61
62
|
}
|
|
62
|
-
constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager, onSessionChannelCreated) {
|
|
63
|
+
constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager, onSessionChannelCreated, ensureIdeRunning) {
|
|
63
64
|
this.bindingRepo = bindingRepo;
|
|
64
65
|
this.chatSessionRepo = chatSessionRepo;
|
|
65
66
|
this.workspaceService = workspaceService;
|
|
66
67
|
this.channelManager = channelManager;
|
|
67
68
|
this.onSessionChannelCreated = onSessionChannelCreated;
|
|
69
|
+
this.ensureIdeRunning = ensureIdeRunning;
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
72
|
* /project list -- Display project list via select menu
|
|
71
73
|
*/
|
|
72
74
|
async handleShow(interaction) {
|
|
75
|
+
await this.ensureIdeRunning?.({
|
|
76
|
+
channelId: interaction.channelId,
|
|
77
|
+
userId: interaction.user.id,
|
|
78
|
+
});
|
|
73
79
|
const workspaces = this.workspaceService.scanWorkspaces();
|
|
74
80
|
const { embeds, components } = (0, projectListUi_1.buildProjectListUI)(workspaces, 0);
|
|
75
81
|
await interaction.editReply({ embeds, components });
|
|
@@ -33,11 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkPort = checkPort;
|
|
37
|
+
exports.startAntigravity = startAntigravity;
|
|
38
|
+
exports.stopAntigravity = stopAntigravity;
|
|
36
39
|
exports.ensureAntigravityRunning = ensureAntigravityRunning;
|
|
37
40
|
const logger_1 = require("../utils/logger");
|
|
38
41
|
const cdpPorts_1 = require("../utils/cdpPorts");
|
|
39
42
|
const pathUtils_1 = require("../utils/pathUtils");
|
|
40
43
|
const http = __importStar(require("http"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
let lifecycleOperation = null;
|
|
41
46
|
/**
|
|
42
47
|
* Check if CDP responds on the specified port.
|
|
43
48
|
*/
|
|
@@ -63,6 +68,131 @@ function checkPort(port) {
|
|
|
63
68
|
});
|
|
64
69
|
});
|
|
65
70
|
}
|
|
71
|
+
async function waitForPort(port, timeoutMs = 30000) {
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
do {
|
|
74
|
+
if (await checkPort(port))
|
|
75
|
+
return true;
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
77
|
+
} while (Date.now() < deadline);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function serializeLifecycle(operation) {
|
|
81
|
+
const pending = lifecycleOperation
|
|
82
|
+
? lifecycleOperation.then(operation, operation)
|
|
83
|
+
: operation();
|
|
84
|
+
lifecycleOperation = pending;
|
|
85
|
+
pending.finally(() => {
|
|
86
|
+
if (lifecycleOperation === pending)
|
|
87
|
+
lifecycleOperation = null;
|
|
88
|
+
}).catch(() => { });
|
|
89
|
+
return pending;
|
|
90
|
+
}
|
|
91
|
+
function startAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
92
|
+
return serializeLifecycle(async () => {
|
|
93
|
+
if (await checkPort(port))
|
|
94
|
+
return 'already-running';
|
|
95
|
+
const executable = process.env.ANTIGRAVITY_PATH || (0, pathUtils_1.getAntigravityCliPath)();
|
|
96
|
+
const child = (0, child_process_1.spawn)(executable, [`--remote-debugging-port=${port}`], {
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: 'ignore',
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
const spawnError = await new Promise((resolve) => {
|
|
102
|
+
child.on('error', resolve);
|
|
103
|
+
setTimeout(() => resolve(null), 100);
|
|
104
|
+
});
|
|
105
|
+
if (spawnError) {
|
|
106
|
+
throw spawnError;
|
|
107
|
+
}
|
|
108
|
+
child.unref();
|
|
109
|
+
if (!await waitForPort(port)) {
|
|
110
|
+
throw new Error(`Antigravity did not become ready on CDP port ${port}.`);
|
|
111
|
+
}
|
|
112
|
+
return 'started';
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function stopAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
116
|
+
return serializeLifecycle(async () => {
|
|
117
|
+
if (!await checkPort(port))
|
|
118
|
+
return 'already-stopped';
|
|
119
|
+
const version = await getCdpVersion(port);
|
|
120
|
+
const browser = typeof version?.Browser === 'string' ? version.Browser.toLowerCase() : '';
|
|
121
|
+
if (!browser.includes('antigravity')) {
|
|
122
|
+
throw new Error(`Refusing to stop non-Antigravity CDP service on port ${port}.`);
|
|
123
|
+
}
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
const command = [
|
|
127
|
+
`$ownerPid=(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue`,
|
|
128
|
+
'| Select-Object -First 1 -ExpandProperty OwningProcess);',
|
|
129
|
+
'if ($ownerPid) { Stop-Process -Id $ownerPid -Force }',
|
|
130
|
+
].join(' ');
|
|
131
|
+
(0, child_process_1.execFile)('powershell.exe', ['-NoProfile', '-Command', command], { windowsHide: true }, (error) => {
|
|
132
|
+
if (error)
|
|
133
|
+
reject(error);
|
|
134
|
+
else
|
|
135
|
+
resolve();
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
(0, child_process_1.execFile)('lsof', ['-tiTCP:' + port, '-sTCP:LISTEN'], (lookupError, stdout) => {
|
|
140
|
+
if (lookupError)
|
|
141
|
+
return reject(lookupError);
|
|
142
|
+
const pid = String(stdout).trim().split(/\s+/)[0];
|
|
143
|
+
if (!/^\d+$/.test(pid))
|
|
144
|
+
return reject(new Error(`No listener PID found for CDP port ${port}.`));
|
|
145
|
+
(0, child_process_1.execFile)('kill', ['-TERM', pid], (killError) => killError ? reject(killError) : resolve());
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
return 'stopped';
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function getCdpVersion(port) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
|
|
154
|
+
let data = '';
|
|
155
|
+
res.on('data', (chunk) => (data += chunk));
|
|
156
|
+
res.on('end', () => {
|
|
157
|
+
try {
|
|
158
|
+
resolve(JSON.parse(data));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
reject(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
req.setTimeout(2000, () => {
|
|
167
|
+
req.destroy();
|
|
168
|
+
reject(new Error(`Timed out reading CDP version from port ${port}.`));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function getCdpTargets(port) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const req = http.get(`http://127.0.0.1:${port}/json/list`, (res) => {
|
|
175
|
+
let data = '';
|
|
176
|
+
res.on('data', (chunk) => (data += chunk));
|
|
177
|
+
res.on('end', () => {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(data);
|
|
180
|
+
if (!Array.isArray(parsed))
|
|
181
|
+
throw new Error('CDP target list is not an array');
|
|
182
|
+
resolve(parsed);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
reject(error);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
req.on('error', reject);
|
|
190
|
+
req.setTimeout(2000, () => {
|
|
191
|
+
req.destroy();
|
|
192
|
+
reject(new Error(`Timed out reading CDP metadata from port ${port}.`));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
66
196
|
/**
|
|
67
197
|
* Check if Antigravity is running with CDP ports.
|
|
68
198
|
* If not running, output a warning log (no auto-start or restart).
|
|
@@ -30,6 +30,8 @@ function classifyAssistantSegments(payload) {
|
|
|
30
30
|
diagnostics: {
|
|
31
31
|
source: 'legacy-fallback',
|
|
32
32
|
segmentCounts: {},
|
|
33
|
+
allFingerprints: [],
|
|
34
|
+
totalSegments: 0,
|
|
33
35
|
fallbackReason: 'invalid-payload',
|
|
34
36
|
},
|
|
35
37
|
};
|
|
@@ -40,12 +42,15 @@ function classifyAssistantSegments(payload) {
|
|
|
40
42
|
const activityLines = [];
|
|
41
43
|
const feedbackTexts = [];
|
|
42
44
|
const segmentCounts = {};
|
|
45
|
+
const allFingerprints = [];
|
|
43
46
|
for (const seg of segments) {
|
|
44
47
|
segmentCounts[seg.kind] = (segmentCounts[seg.kind] ?? 0) + 1;
|
|
45
48
|
switch (seg.kind) {
|
|
46
49
|
case 'assistant-body':
|
|
47
50
|
if (seg.text && seg.text.trim()) {
|
|
48
51
|
bodyTexts.push(seg.text);
|
|
52
|
+
const fp = seg.text.length + ':' + seg.text.slice(0, 50) + ':' + seg.text.slice(-50);
|
|
53
|
+
allFingerprints.push(fp);
|
|
49
54
|
}
|
|
50
55
|
break;
|
|
51
56
|
case 'thinking':
|
|
@@ -62,9 +67,10 @@ function classifyAssistantSegments(payload) {
|
|
|
62
67
|
break;
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const
|
|
70
|
+
// Concatenate all body segments — the CDP script may yield multiple nodes
|
|
71
|
+
// (streaming partial + final, multi-block markdown, etc.)
|
|
72
|
+
const lastBody = bodyTexts.join('\n\n') || '';
|
|
73
|
+
const finalOutputText = (0, htmlToDiscordMarkdown_1.htmlToDiscordMarkdown)(lastBody);
|
|
68
74
|
return {
|
|
69
75
|
finalOutputText,
|
|
70
76
|
activityLines,
|
|
@@ -72,6 +78,8 @@ function classifyAssistantSegments(payload) {
|
|
|
72
78
|
diagnostics: {
|
|
73
79
|
source: 'dom-structured',
|
|
74
80
|
segmentCounts,
|
|
81
|
+
allFingerprints,
|
|
82
|
+
totalSegments: segments.length,
|
|
75
83
|
},
|
|
76
84
|
};
|
|
77
85
|
}
|
|
@@ -100,9 +108,9 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
100
108
|
|
|
101
109
|
// Same selectors as RESPONSE_TEXT — ordered by specificity
|
|
102
110
|
var selectors = [
|
|
111
|
+
'.text-ide-message-block-bot-color',
|
|
103
112
|
'.rendered-markdown',
|
|
104
113
|
'.leading-relaxed.select-text',
|
|
105
|
-
'.flex.flex-col.gap-y-3',
|
|
106
114
|
'[data-message-author-role="assistant"]',
|
|
107
115
|
'[data-message-role="assistant"]',
|
|
108
116
|
'[class*="assistant-message"]',
|
|
@@ -138,6 +146,12 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
138
146
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
139
147
|
if (node.closest('.notify-user-container')) return true;
|
|
140
148
|
if (node.closest('[role="dialog"]')) return true;
|
|
149
|
+
if (node.closest('form')) return true;
|
|
150
|
+
if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
|
|
151
|
+
if (node.querySelector('textarea') || node.closest('textarea')) return true;
|
|
152
|
+
var text = (node.innerText || '').toLowerCase();
|
|
153
|
+
if (text.includes('ask anything, @ to mention')) return true;
|
|
154
|
+
if (text.includes('0 files with changes')) return true;
|
|
141
155
|
return false;
|
|
142
156
|
};
|
|
143
157
|
|
|
@@ -221,10 +235,43 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
221
235
|
text: bodyHtml,
|
|
222
236
|
role: 'assistant',
|
|
223
237
|
messageIndex: 0,
|
|
224
|
-
domPath: '
|
|
238
|
+
domPath: 'article:nth(' + i + ')'
|
|
225
239
|
});
|
|
226
240
|
bodyFound = true;
|
|
227
|
-
break; // Only
|
|
241
|
+
break; // Only capture the single latest assistant message
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Pass 3: Thinking logs and tool calls
|
|
246
|
+
var thinkingNodes = scope.querySelectorAll('[class*="thinking"], [class*="thought"]');
|
|
247
|
+
for (var j = 0; j < thinkingNodes.length; j++) {
|
|
248
|
+
var tnode = thinkingNodes[j];
|
|
249
|
+
if (isInsideExcludedContainer(tnode)) continue;
|
|
250
|
+
var ttext = (tnode.innerText || tnode.textContent || '').trim();
|
|
251
|
+
if (ttext) {
|
|
252
|
+
segments.push({
|
|
253
|
+
kind: 'thinking',
|
|
254
|
+
text: ttext,
|
|
255
|
+
role: 'assistant',
|
|
256
|
+
messageIndex: 0,
|
|
257
|
+
domPath: 'thinking:nth(' + j + ')'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var toolNodes = scope.querySelectorAll('[class*="tool-interaction"], [class*="call-summary"]');
|
|
263
|
+
for (var k = 0; k < toolNodes.length; k++) {
|
|
264
|
+
var knode = toolNodes[k];
|
|
265
|
+
if (isInsideExcludedContainer(knode)) continue;
|
|
266
|
+
var ktext = (knode.innerText || knode.textContent || '').trim();
|
|
267
|
+
if (ktext) {
|
|
268
|
+
segments.push({
|
|
269
|
+
kind: 'tool-call',
|
|
270
|
+
text: ktext,
|
|
271
|
+
role: 'assistant',
|
|
272
|
+
messageIndex: 0,
|
|
273
|
+
domPath: 'tool:nth(' + k + ')'
|
|
274
|
+
});
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
|
|
@@ -229,7 +229,7 @@ function parseRunCommandCustomId(customId) {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
/** Initialize the CDP bridge (lazy connection: pool creation only) */
|
|
232
|
-
function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDirs = {}) {
|
|
232
|
+
function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDirs = {}, cdpHost = '127.0.0.1') {
|
|
233
233
|
const pool = new cdpConnectionPool_1.CdpConnectionPool({
|
|
234
234
|
accountPorts,
|
|
235
235
|
accountUserDataDirs,
|
|
@@ -238,6 +238,7 @@ function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDir
|
|
|
238
238
|
// Reconnection is triggered when the next chat/template message is sent.
|
|
239
239
|
maxReconnectAttempts: 0,
|
|
240
240
|
reconnectDelayMs: 3000,
|
|
241
|
+
cdpHost,
|
|
241
242
|
});
|
|
242
243
|
const quota = new quotaService_1.QuotaService();
|
|
243
244
|
const autoAccept = new autoAcceptService_1.AutoAcceptService(autoApproveDefault);
|
|
@@ -250,6 +251,7 @@ function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDir
|
|
|
250
251
|
approvalChannelByWorkspace: new Map(),
|
|
251
252
|
approvalChannelBySession: new Map(),
|
|
252
253
|
selectedAccountByChannel: new Map(),
|
|
254
|
+
cdpHost,
|
|
253
255
|
};
|
|
254
256
|
}
|
|
255
257
|
/**
|
|
@@ -526,6 +528,14 @@ function ensureRunCommandDetector(bridge, cdp, projectName, accountName = 'defau
|
|
|
526
528
|
function ensureUserMessageDetector(bridge, cdp, projectName, onUserMessage, accountName = 'default') {
|
|
527
529
|
const existing = bridge.pool.getUserMessageDetector(projectName, accountName);
|
|
528
530
|
if (existing && existing.isActive()) {
|
|
531
|
+
// Remove only the listeners we may have previously attached via this
|
|
532
|
+
// code path. Using removeAllListeners('message') would wipe any
|
|
533
|
+
// subscriber added elsewhere, so we track and swap explicitly.
|
|
534
|
+
const prevListener = existing.__userMsgHandler;
|
|
535
|
+
if (prevListener) {
|
|
536
|
+
existing.off('message', prevListener);
|
|
537
|
+
}
|
|
538
|
+
existing.__userMsgHandler = onUserMessage;
|
|
529
539
|
existing.on('message', onUserMessage);
|
|
530
540
|
return;
|
|
531
541
|
}
|
|
@@ -533,6 +543,7 @@ function ensureUserMessageDetector(bridge, cdp, projectName, onUserMessage, acco
|
|
|
533
543
|
cdpService: cdp,
|
|
534
544
|
pollIntervalMs: 2000,
|
|
535
545
|
});
|
|
546
|
+
detector.__userMsgHandler = onUserMessage;
|
|
536
547
|
detector.on('message', onUserMessage);
|
|
537
548
|
detector.start();
|
|
538
549
|
bridge.pool.registerUserMessageDetector(projectName, detector, accountName);
|
|
@@ -47,7 +47,10 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
47
47
|
/** Antigravity UI DOM selector constants */
|
|
48
48
|
const SELECTORS = {
|
|
49
49
|
/** Chat input box: textbox excluding xterm */
|
|
50
|
-
CHAT_INPUT:
|
|
50
|
+
CHAT_INPUT: [
|
|
51
|
+
'div[role="textbox"]:not(.xterm-helper-textarea)',
|
|
52
|
+
'div[role="combobox"][contenteditable="true"][aria-label="Message input"]',
|
|
53
|
+
].join(', '),
|
|
51
54
|
/** Submit button search target tag */
|
|
52
55
|
SUBMIT_BUTTON_CONTAINER: 'button',
|
|
53
56
|
/** Submit icon SVG class candidates */
|
|
@@ -61,11 +64,15 @@ const WORKSPACE_STATE_SCRIPT = `(() => {
|
|
|
61
64
|
|
|
62
65
|
let isGenerating = false;
|
|
63
66
|
for (const scope of scopes) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const els = scope.querySelectorAll('[data-tooltip-id="input-send-button-cancel-tooltip"]');
|
|
68
|
+
for (let i = 0; i < els.length; i++) {
|
|
69
|
+
const style = window.getComputedStyle(els[i]);
|
|
70
|
+
if (style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0) {
|
|
71
|
+
isGenerating = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
68
74
|
}
|
|
75
|
+
if (isGenerating) break;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
if (!isGenerating) {
|
|
@@ -142,6 +149,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
142
149
|
/** Workspace switching flag (suppresses disconnected event) */
|
|
143
150
|
isSwitchingWorkspace = false;
|
|
144
151
|
accountName;
|
|
152
|
+
cdpHost;
|
|
145
153
|
accountPorts;
|
|
146
154
|
accountUserDataDirs;
|
|
147
155
|
constructor(options = {}) {
|
|
@@ -154,6 +162,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
154
162
|
this.cdpCallTimeout = options.cdpCallTimeout;
|
|
155
163
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
|
|
156
164
|
this.reconnectDelayMs = options.reconnectDelayMs ?? 2000;
|
|
165
|
+
this.cdpHost = options.cdpHost ?? '127.0.0.1';
|
|
157
166
|
}
|
|
158
167
|
resolveAccountPorts(accountName) {
|
|
159
168
|
const explicitPort = this.accountPorts[accountName];
|
|
@@ -189,7 +198,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
189
198
|
let allPages = [];
|
|
190
199
|
for (const port of this.ports) {
|
|
191
200
|
try {
|
|
192
|
-
const list = await this.getJson(`http
|
|
201
|
+
const list = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
193
202
|
allPages.push(...list);
|
|
194
203
|
}
|
|
195
204
|
catch (e) {
|
|
@@ -466,7 +475,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
466
475
|
let respondingPort = null;
|
|
467
476
|
for (const port of this.ports) {
|
|
468
477
|
try {
|
|
469
|
-
const list = await this.getJson(`http
|
|
478
|
+
const list = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
470
479
|
pages.push(...list);
|
|
471
480
|
// Prioritize recording ports that contain workbench pages
|
|
472
481
|
const hasWorkbench = list.some((t) => t.url?.includes('workbench'));
|
|
@@ -557,8 +566,8 @@ class CdpService extends events_1.EventEmitter {
|
|
|
557
566
|
logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
|
|
558
567
|
return true;
|
|
559
568
|
}
|
|
560
|
-
// If title
|
|
561
|
-
if (
|
|
569
|
+
// If title match failed, or if it's "Untitled", verify by folder path
|
|
570
|
+
if (workspacePath) {
|
|
562
571
|
const folderMatch = await this.probeWorkspaceFolderPath(projectName, workspacePath);
|
|
563
572
|
if (folderMatch) {
|
|
564
573
|
return true;
|
|
@@ -708,7 +717,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
708
717
|
let knownPageIds = new Set();
|
|
709
718
|
for (const port of this.ports) {
|
|
710
719
|
try {
|
|
711
|
-
const preLaunchPages = await this.getJson(`http
|
|
720
|
+
const preLaunchPages = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
712
721
|
preLaunchPages.forEach((p) => {
|
|
713
722
|
if (p.id)
|
|
714
723
|
knownPageIds.add(p.id);
|
|
@@ -723,7 +732,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
723
732
|
let pages = [];
|
|
724
733
|
for (const port of this.ports) {
|
|
725
734
|
try {
|
|
726
|
-
const list = await this.getJson(`http
|
|
735
|
+
const list = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
727
736
|
pages.push(...list);
|
|
728
737
|
}
|
|
729
738
|
catch {
|
|
@@ -767,7 +776,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
767
776
|
let allPages = [];
|
|
768
777
|
for (const port of this.ports) {
|
|
769
778
|
try {
|
|
770
|
-
const list = await this.getJson(`http
|
|
779
|
+
const list = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
771
780
|
allPages.push(...list);
|
|
772
781
|
}
|
|
773
782
|
catch {
|
|
@@ -993,7 +1002,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
993
1002
|
let stillOpen = false;
|
|
994
1003
|
for (const port of this.ports) {
|
|
995
1004
|
try {
|
|
996
|
-
const list = await this.getJson(`http
|
|
1005
|
+
const list = await this.getJson(`http://${this.cdpHost}:${port}/json/list`);
|
|
997
1006
|
if (list.some((page) => page?.id === closingTargetId)) {
|
|
998
1007
|
stillOpen = true;
|
|
999
1008
|
break;
|
|
@@ -1290,6 +1299,12 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1290
1299
|
}
|
|
1291
1300
|
async retryInjectOnce(text, firstError, imageFilePaths) {
|
|
1292
1301
|
logger_1.logger.warn(`[CdpService] Initial message injection failed: ${firstError}. Retrying once after readiness check...`);
|
|
1302
|
+
if (this.isTransientInjectError(firstError)) {
|
|
1303
|
+
const target = await this.findWorkbenchTarget();
|
|
1304
|
+
if (target?.webSocketDebuggerUrl) {
|
|
1305
|
+
await this.openChatPanelViaKeyboard(target.webSocketDebuggerUrl);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1293
1308
|
try {
|
|
1294
1309
|
await this.reconnectOnDemand(INJECT_RETRY_READY_TIMEOUT_MS);
|
|
1295
1310
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChatSessionService = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
4
5
|
/** Script to get the state of the new chat button */
|
|
5
6
|
const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
|
|
6
7
|
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
|
|
@@ -25,7 +26,14 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
|
25
26
|
const header = panel.querySelector('div[class*="border-b"]');
|
|
26
27
|
if (!header) return { title: '', hasActiveChat: false };
|
|
27
28
|
const titleEl = header.querySelector('div[class*="text-ellipsis"]');
|
|
28
|
-
|
|
29
|
+
let title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
30
|
+
if (!title || title === 'Agent') {
|
|
31
|
+
const activeRow = Array.from(panel.querySelectorAll('div[class*="focusBackground"]'))
|
|
32
|
+
.find((el) => el instanceof HTMLElement && el.offsetParent !== null);
|
|
33
|
+
const activeTitle = activeRow?.querySelector('span.text-sm span, span.text-sm');
|
|
34
|
+
const activeText = activeTitle ? (activeTitle.textContent || '').trim() : '';
|
|
35
|
+
if (activeText) title = activeText;
|
|
36
|
+
}
|
|
29
37
|
// "Agent" is the default empty chat title
|
|
30
38
|
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
31
39
|
return { title: title || '(Untitled)', hasActiveChat };
|
|
@@ -44,7 +52,14 @@ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
|
|
|
44
52
|
}
|
|
45
53
|
const header = panel.querySelector('div[class*="border-b"]');
|
|
46
54
|
const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
|
|
47
|
-
|
|
55
|
+
let title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
56
|
+
if (!title || title === 'Agent') {
|
|
57
|
+
const activeRow = Array.from(panel.querySelectorAll('div[class*="focusBackground"]'))
|
|
58
|
+
.find((el) => el instanceof HTMLElement && el.offsetParent !== null);
|
|
59
|
+
const activeTitle = activeRow?.querySelector('span.text-sm span, span.text-sm');
|
|
60
|
+
const activeText = activeTitle ? (activeTitle.textContent || '').trim() : '';
|
|
61
|
+
if (activeText) title = activeText;
|
|
62
|
+
}
|
|
48
63
|
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
49
64
|
|
|
50
65
|
const bodyCandidates = Array.from(panel.querySelectorAll(
|
|
@@ -461,15 +476,21 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
461
476
|
}
|
|
462
477
|
|
|
463
478
|
let selected = clickByPatterns([wanted, wantedLoose], '[role="option"], li, button, [data-testid*="conversation"]');
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
479
|
+
if (selected) {
|
|
480
|
+
const selectedOption = pickBest(
|
|
481
|
+
asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
|
|
482
|
+
[wanted, wantedLoose],
|
|
483
|
+
);
|
|
484
|
+
const selectedClickable = getClickable(selectedOption);
|
|
485
|
+
if (selectedClickable) {
|
|
486
|
+
selectedClickable.focus();
|
|
487
|
+
pressEnter(selectedClickable);
|
|
488
|
+
}
|
|
468
489
|
}
|
|
469
490
|
if (!selected) {
|
|
470
491
|
return { ok: false, error: 'Conversation not found in Past Conversations' };
|
|
471
492
|
}
|
|
472
|
-
return { ok: true };
|
|
493
|
+
return { ok: true, matchedTitle: wantedRaw };
|
|
473
494
|
})();
|
|
474
495
|
})()`;
|
|
475
496
|
}
|
|
@@ -878,6 +899,18 @@ class ChatSessionService {
|
|
|
878
899
|
}
|
|
879
900
|
return { ok: true };
|
|
880
901
|
}
|
|
902
|
+
// Current Antigravity builds keep the header at the generic "Agent"
|
|
903
|
+
// label even after an exact Past Conversations option is selected.
|
|
904
|
+
// The selection script only reports success after matching the wanted
|
|
905
|
+
// title inside this workspace's conversation picker.
|
|
906
|
+
if (usedPastConversations &&
|
|
907
|
+
after.title.trim() === 'Agent' &&
|
|
908
|
+
pastResult?.matchedTitle?.trim() === title.trim()) {
|
|
909
|
+
logger_1.logger.debug(`[ChatSession] Accepting Agent-header bypass: ` +
|
|
910
|
+
`usedPastConversations=true observedTitle="${after.title}" targetTitle="${title}"`);
|
|
911
|
+
await this.closePanelWithEscape(cdpService);
|
|
912
|
+
return { ok: true };
|
|
913
|
+
}
|
|
881
914
|
if (!warmupConsumed && allowVisibilityWarmupMs > 0 && after.title.trim() === 'Agent') {
|
|
882
915
|
warmupConsumed = true;
|
|
883
916
|
startedAt = Date.now();
|
|
@@ -898,6 +931,13 @@ class ChatSessionService {
|
|
|
898
931
|
await this.closePanelWithEscape(cdpService);
|
|
899
932
|
return { ok: true };
|
|
900
933
|
}
|
|
934
|
+
if (afterPast.title.trim() === 'Agent' &&
|
|
935
|
+
viaPast.matchedTitle?.trim() === title.trim()) {
|
|
936
|
+
logger_1.logger.debug(`[ChatSession] Accepting Agent-header bypass: ` +
|
|
937
|
+
`usedPastConversations=true observedTitle="${afterPast.title}" targetTitle="${title}"`);
|
|
938
|
+
await this.closePanelWithEscape(cdpService);
|
|
939
|
+
return { ok: true };
|
|
940
|
+
}
|
|
901
941
|
return {
|
|
902
942
|
ok: false,
|
|
903
943
|
error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
|
|
@@ -933,7 +973,12 @@ class ChatSessionService {
|
|
|
933
973
|
});
|
|
934
974
|
const value = result?.result?.value;
|
|
935
975
|
if (value?.ok) {
|
|
936
|
-
return {
|
|
976
|
+
return {
|
|
977
|
+
ok: true,
|
|
978
|
+
...(typeof value.matchedTitle === 'string'
|
|
979
|
+
? { matchedTitle: value.matchedTitle }
|
|
980
|
+
: {}),
|
|
981
|
+
};
|
|
937
982
|
}
|
|
938
983
|
if (value?.error && typeof value.error === 'string') {
|
|
939
984
|
lastError = value.error;
|
|
@@ -15,9 +15,9 @@ exports.RESPONSE_SELECTORS = {
|
|
|
15
15
|
const scopes = [panel, document].filter(Boolean);
|
|
16
16
|
|
|
17
17
|
const selectors = [
|
|
18
|
+
{ sel: '.text-ide-message-block-bot-color', score: 11 },
|
|
18
19
|
{ sel: '.rendered-markdown', score: 10 },
|
|
19
20
|
{ sel: '.leading-relaxed.select-text', score: 9 },
|
|
20
|
-
{ sel: '.flex.flex-col.gap-y-3', score: 8 },
|
|
21
21
|
{ sel: '[data-message-author-role="assistant"]', score: 7 },
|
|
22
22
|
{ sel: '[data-message-role="assistant"]', score: 6 },
|
|
23
23
|
{ sel: '[class*="assistant-message"]', score: 5 },
|
|
@@ -47,6 +47,12 @@ exports.RESPONSE_SELECTORS = {
|
|
|
47
47
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
48
48
|
if (node.closest('.notify-user-container')) return true;
|
|
49
49
|
if (node.closest('[role="dialog"]')) return true;
|
|
50
|
+
if (node.closest('form')) return true;
|
|
51
|
+
if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
|
|
52
|
+
if (node.querySelector('textarea') || node.closest('textarea')) return true;
|
|
53
|
+
var text = (node.innerText || '').toLowerCase();
|
|
54
|
+
if (text.includes('ask anything, @ to mention')) return true;
|
|
55
|
+
if (text.includes('0 files with changes')) return true;
|
|
50
56
|
return false;
|
|
51
57
|
};
|
|
52
58
|
|
|
@@ -99,8 +105,16 @@ exports.RESPONSE_SELECTORS = {
|
|
|
99
105
|
const scopes = [panel, document].filter(Boolean);
|
|
100
106
|
|
|
101
107
|
for (const scope of scopes) {
|
|
102
|
-
const
|
|
103
|
-
|
|
108
|
+
const els = scope.querySelectorAll('[data-tooltip-id="input-send-button-cancel-tooltip"]');
|
|
109
|
+
for (let i = 0; i < els.length; i++) {
|
|
110
|
+
const el = els[i];
|
|
111
|
+
if (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0) {
|
|
112
|
+
const style = window.getComputedStyle(el);
|
|
113
|
+
if (style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0) {
|
|
114
|
+
return { isGenerating: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
@@ -140,10 +154,16 @@ exports.RESPONSE_SELECTORS = {
|
|
|
140
154
|
const scopes = [panel, document].filter(Boolean);
|
|
141
155
|
|
|
142
156
|
for (const scope of scopes) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
el
|
|
146
|
-
|
|
157
|
+
const els = scope.querySelectorAll('[data-tooltip-id="input-send-button-cancel-tooltip"]');
|
|
158
|
+
for (let i = 0; i < els.length; i++) {
|
|
159
|
+
const el = els[i];
|
|
160
|
+
if (el && typeof el.click === 'function') {
|
|
161
|
+
const style = window.getComputedStyle(el);
|
|
162
|
+
if (style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0) {
|
|
163
|
+
el.click();
|
|
164
|
+
return { ok: true, method: 'tooltip-id' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
147
167
|
}
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -185,9 +205,9 @@ exports.RESPONSE_SELECTORS = {
|
|
|
185
205
|
const scopes = [panel, document].filter(Boolean);
|
|
186
206
|
|
|
187
207
|
const selectors = [
|
|
208
|
+
{ sel: '.text-ide-message-block-bot-color', score: 11 },
|
|
188
209
|
{ sel: '.rendered-markdown', score: 10 },
|
|
189
210
|
{ sel: '.leading-relaxed.select-text', score: 9 },
|
|
190
|
-
{ sel: '.flex.flex-col.gap-y-3', score: 8 },
|
|
191
211
|
{ sel: '[data-message-author-role="assistant"]', score: 7 },
|
|
192
212
|
{ sel: '[data-message-role="assistant"]', score: 6 },
|
|
193
213
|
{ sel: '[class*="assistant-message"]', score: 5 },
|
|
@@ -215,6 +235,12 @@ exports.RESPONSE_SELECTORS = {
|
|
|
215
235
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
216
236
|
if (node.closest('.notify-user-container')) return true;
|
|
217
237
|
if (node.closest('[role="dialog"]')) return true;
|
|
238
|
+
if (node.closest('form')) return true;
|
|
239
|
+
if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
|
|
240
|
+
if (node.querySelector('textarea') || node.closest('textarea')) return true;
|
|
241
|
+
var text = (node.innerText || '').toLowerCase();
|
|
242
|
+
if (text.includes('ask anything, @ to mention')) return true;
|
|
243
|
+
if (text.includes('0 files with changes')) return true;
|
|
218
244
|
return false;
|
|
219
245
|
};
|
|
220
246
|
const looksLikeToolOutput = (text) => {
|
|
@@ -271,9 +297,9 @@ exports.RESPONSE_SELECTORS = {
|
|
|
271
297
|
const scopes = [panel, document].filter(Boolean);
|
|
272
298
|
|
|
273
299
|
const selectors = [
|
|
300
|
+
{ sel: '.text-ide-message-block-bot-color', score: 11 },
|
|
274
301
|
{ sel: '.rendered-markdown', score: 10 },
|
|
275
302
|
{ sel: '.leading-relaxed.select-text', score: 9 },
|
|
276
|
-
{ sel: '.flex.flex-col.gap-y-3', score: 8 },
|
|
277
303
|
{ sel: '[data-message-author-role="assistant"]', score: 7 },
|
|
278
304
|
{ sel: '[data-message-role="assistant"]', score: 6 },
|
|
279
305
|
{ sel: '[class*="assistant-message"]', score: 5 },
|
|
@@ -308,6 +334,12 @@ exports.RESPONSE_SELECTORS = {
|
|
|
308
334
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
309
335
|
if (node.closest('.notify-user-container')) return true;
|
|
310
336
|
if (node.closest('[role="dialog"]')) return true;
|
|
337
|
+
if (node.closest('form')) return true;
|
|
338
|
+
if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
|
|
339
|
+
if (node.querySelector('textarea') || node.closest('textarea')) return true;
|
|
340
|
+
var text = (node.innerText || '').toLowerCase();
|
|
341
|
+
if (text.includes('ask anything, @ to mention')) return true;
|
|
342
|
+
if (text.includes('0 files with changes')) return true;
|
|
311
343
|
return false;
|
|
312
344
|
};
|
|
313
345
|
|
|
@@ -25,38 +25,31 @@ const DETECT_USER_MESSAGE_SCRIPT = `(() => {
|
|
|
25
25
|
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
26
26
|
const scope = panel || document;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'[
|
|
33
|
-
|
|
28
|
+
const bubbles = Array.from(scope.querySelectorAll('.bg-input.p-2'));
|
|
29
|
+
const userBubbles = bubbles.filter(el => {
|
|
30
|
+
if (el.closest('.text-ide-message-block-bot-color')) return false;
|
|
31
|
+
if (el.closest('.rendered-markdown, .prose')) return false;
|
|
32
|
+
if (el.closest('[data-message-author-role="assistant"], [data-message-role="assistant"]')) return false;
|
|
33
|
+
return !!el.querySelector('.whitespace-pre-wrap');
|
|
34
|
+
});
|
|
34
35
|
|
|
35
|
-
if (
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (userBubbles.length > 0) {
|
|
37
|
+
const lastBubble = userBubbles[userBubbles.length - 1];
|
|
38
|
+
// Surgical extraction: only the content of the message div
|
|
39
|
+
const textEl = lastBubble.querySelector('.whitespace-pre-wrap');
|
|
40
|
+
if (textEl) {
|
|
41
|
+
// Clone to strip buttons without affecting the real UI
|
|
42
|
+
const clone = textEl.cloneNode(true);
|
|
43
|
+
const buttons = clone.querySelectorAll('button, [role="button"]');
|
|
44
|
+
buttons.forEach(b => b.remove());
|
|
45
|
+
|
|
46
|
+
let text = (clone.textContent || '').trim();
|
|
47
|
+
// Final safety strip
|
|
48
|
+
text = text.replace(/\\s*(?:Undo|撤銷|撤销|元に戻す)\\s*$/i, '').trim();
|
|
49
|
+
if (text.length > 0) return { text };
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
// Strategy B (fallback): Find individual bubble containers, filtering out
|
|
42
|
-
// any element that itself contains nested bubble elements (i.e., a parent wrapper).
|
|
43
|
-
const userBubbles = Array.from(scope.querySelectorAll(
|
|
44
|
-
'[class*="bg-gray-500/15"][class*="rounded-lg"][class*="select-text"]'
|
|
45
|
-
)).filter(el => !el.querySelector('[class*="bg-gray-500/15"][class*="select-text"]'));
|
|
46
|
-
|
|
47
|
-
if (userBubbles.length === 0) return null;
|
|
48
|
-
|
|
49
|
-
const lastBubble = userBubbles[userBubbles.length - 1];
|
|
50
|
-
const textEl = lastBubble.querySelector('.whitespace-pre-wrap')
|
|
51
|
-
|| lastBubble.querySelector('[style*="word-break"]');
|
|
52
|
-
|
|
53
|
-
const text = textEl
|
|
54
|
-
? (textEl.textContent || '').trim()
|
|
55
|
-
: (lastBubble.textContent || '').trim();
|
|
56
|
-
|
|
57
|
-
if (!text || text.length < 1) return null;
|
|
58
|
-
|
|
59
|
-
return { text };
|
|
52
|
+
return null;
|
|
60
53
|
})()`;
|
|
61
54
|
/**
|
|
62
55
|
* Normalize text for echo hash comparison.
|
|
@@ -87,6 +80,8 @@ class UserMessageDetector extends events_1.EventEmitter {
|
|
|
87
80
|
/** Set of all previously detected message hashes (defense-in-depth dedup) */
|
|
88
81
|
seenHashes = new Set();
|
|
89
82
|
static MAX_SEEN_HASHES = 50;
|
|
83
|
+
/** The actual text of the last emitted message (extra safety dedup) */
|
|
84
|
+
lastSentText = null;
|
|
90
85
|
/** True during the first poll — seeds existing DOM state without firing callback */
|
|
91
86
|
isPriming = false;
|
|
92
87
|
constructor(options) {
|
|
@@ -106,12 +101,12 @@ class UserMessageDetector extends events_1.EventEmitter {
|
|
|
106
101
|
this.echoHashes.delete(hash);
|
|
107
102
|
}, 60000);
|
|
108
103
|
}
|
|
109
|
-
/** Start monitoring. The first poll seeds the current DOM state without firing the callback. */
|
|
110
104
|
start() {
|
|
111
105
|
if (this.isRunning)
|
|
112
106
|
return;
|
|
113
107
|
this.isRunning = true;
|
|
114
108
|
this.lastDetectedHash = null;
|
|
109
|
+
this.lastSentText = null;
|
|
115
110
|
this.seenHashes.clear();
|
|
116
111
|
this.isPriming = true;
|
|
117
112
|
// echoHashes are intentionally NOT cleared — they have their own 60s TTL
|
|
@@ -204,7 +199,13 @@ class UserMessageDetector extends events_1.EventEmitter {
|
|
|
204
199
|
this.addToSeenHashes(hash);
|
|
205
200
|
return;
|
|
206
201
|
}
|
|
202
|
+
if (info.text === this.lastSentText) {
|
|
203
|
+
logger_1.logger.debug(`[UserMessageDetector] lastSentText match, skipping: "${preview}..."`);
|
|
204
|
+
this.lastDetectedHash = hash;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
207
|
this.lastDetectedHash = hash;
|
|
208
|
+
this.lastSentText = info.text;
|
|
208
209
|
this.addToSeenHashes(hash);
|
|
209
210
|
logger_1.logger.debug(`[UserMessageDetector] New message detected: "${preview}..."`);
|
|
210
211
|
this.emit('message', info);
|
|
@@ -108,6 +108,8 @@ function mergeConfig(persisted) {
|
|
|
108
108
|
// Telegram credentials — only required when Telegram is an active platform
|
|
109
109
|
const telegramToken = process.env.TELEGRAM_BOT_TOKEN ?? persisted.telegramToken ?? undefined;
|
|
110
110
|
const telegramAllowedUserIds = resolveTelegramAllowedUserIds(persisted);
|
|
111
|
+
const rawCdpHost = (process.env.CDP_HOST ?? persisted.cdpHost ?? '').trim();
|
|
112
|
+
const cdpHost = rawCdpHost || '127.0.0.1';
|
|
111
113
|
if (platforms.includes('telegram') && !telegramToken) {
|
|
112
114
|
throw new Error('TELEGRAM_BOT_TOKEN is required when platforms include "telegram"');
|
|
113
115
|
}
|
|
@@ -125,6 +127,7 @@ function mergeConfig(persisted) {
|
|
|
125
127
|
telegramToken,
|
|
126
128
|
telegramAllowedUserIds,
|
|
127
129
|
platforms,
|
|
130
|
+
cdpHost,
|
|
128
131
|
};
|
|
129
132
|
}
|
|
130
133
|
function resolveAllowedUserIds(persisted) {
|
package/dist/utils/pathUtils.js
CHANGED
|
@@ -22,9 +22,9 @@ function getAntigravityCliPath() {
|
|
|
22
22
|
if (process.platform === 'win32') {
|
|
23
23
|
const localAppData = process.env.LOCALAPPDATA;
|
|
24
24
|
if (localAppData) {
|
|
25
|
-
return `${localAppData}\\Programs\\Antigravity\\Antigravity.exe`;
|
|
25
|
+
return `${localAppData}\\Programs\\Antigravity IDE\\Antigravity IDE.exe`;
|
|
26
26
|
}
|
|
27
|
-
return 'Antigravity.exe'; // Fallback if LOCALAPPDATA is undefined
|
|
27
|
+
return 'Antigravity IDE.exe'; // Fallback if LOCALAPPDATA is undefined
|
|
28
28
|
}
|
|
29
29
|
// Default for Linux or any unknown OS, assuming 'antigravity' is in the system PATH
|
|
30
30
|
return 'antigravity';
|
|
@@ -50,7 +50,7 @@ function getAntigravityCdpHint(port = 9222) {
|
|
|
50
50
|
case 'darwin':
|
|
51
51
|
return `open -a ${APP_NAME} --args --remote-debugging-port=${port}`;
|
|
52
52
|
case 'win32':
|
|
53
|
-
return
|
|
53
|
+
return `"${APP_NAME} IDE.exe" --remote-debugging-port=${port}`;
|
|
54
54
|
default:
|
|
55
55
|
return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
|
|
56
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazy-gravity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@mermaid-js/mermaid-cli": "^11.12.0",
|
|
61
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
62
|
+
"@semantic-release/git": "^10.0.1",
|
|
61
63
|
"@types/better-sqlite3": "^7.6.13",
|
|
62
64
|
"@types/jest": "^30.0.0",
|
|
63
65
|
"@types/node": "^25.3.0",
|