lazy-gravity 0.0.4 → 0.2.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 +22 -7
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +25 -19
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +445 -126
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +40 -0
- package/dist/commands/workspaceCommandHandler.js +17 -28
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +338 -30
- package/dist/events/messageCreateHandler.js +161 -47
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +7 -0
- package/dist/services/assistantDomExtractor.js +339 -0
- package/dist/services/cdpBridgeManager.js +323 -39
- package/dist/services/cdpConnectionPool.js +117 -33
- package/dist/services/cdpService.js +149 -53
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +271 -0
- package/dist/services/planningDetector.js +318 -0
- package/dist/services/responseMonitor.js +308 -70
- package/dist/services/retryStore.js +46 -0
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/buttonUtils.js +33 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/modelsUi.js +24 -13
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/projectListUi.js +83 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +18 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/discordFormatter.js +149 -16
- package/dist/utils/htmlToDiscordMarkdown.js +184 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logFileTransport.js +147 -0
- package/dist/utils/logger.js +86 -21
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/dist/utils/processLogBuffer.js +4 -0
- package/package.json +4 -4
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.COOLDOWN_MS = exports.UPDATE_CHECK_FILE = void 0;
|
|
37
|
+
exports.shouldCheckForUpdates = shouldCheckForUpdates;
|
|
38
|
+
exports.fetchLatestVersion = fetchLatestVersion;
|
|
39
|
+
exports.checkForUpdates = checkForUpdates;
|
|
40
|
+
const https = __importStar(require("https"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const CONFIG_DIR = '.lazy-gravity';
|
|
45
|
+
exports.UPDATE_CHECK_FILE = 'update-check.json';
|
|
46
|
+
exports.COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
47
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/lazy-gravity/latest';
|
|
48
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
49
|
+
function getCachePath() {
|
|
50
|
+
return path.join(os.homedir(), CONFIG_DIR, exports.UPDATE_CHECK_FILE);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Determine whether enough time has elapsed since the last update check.
|
|
54
|
+
* Returns true if we should query the registry.
|
|
55
|
+
*/
|
|
56
|
+
function shouldCheckForUpdates() {
|
|
57
|
+
const cachePath = getCachePath();
|
|
58
|
+
try {
|
|
59
|
+
if (!fs.existsSync(cachePath))
|
|
60
|
+
return true;
|
|
61
|
+
const raw = fs.readFileSync(cachePath, 'utf-8');
|
|
62
|
+
const cache = JSON.parse(raw);
|
|
63
|
+
return Date.now() - cache.lastCheck >= exports.COOLDOWN_MS;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Query the npm registry for the latest published version.
|
|
71
|
+
*/
|
|
72
|
+
function fetchLatestVersion() {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const req = https.get(REGISTRY_URL, (res) => {
|
|
75
|
+
if (res.statusCode !== 200) {
|
|
76
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let body = '';
|
|
80
|
+
res.on('data', (chunk) => {
|
|
81
|
+
body += chunk;
|
|
82
|
+
});
|
|
83
|
+
res.on('end', () => {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(body);
|
|
86
|
+
resolve(data.version);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
req.on('error', reject);
|
|
94
|
+
req.on('timeout', () => {
|
|
95
|
+
req.destroy();
|
|
96
|
+
reject(new Error('Request timed out'));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function writeCache() {
|
|
101
|
+
const cachePath = getCachePath();
|
|
102
|
+
const dir = path.dirname(cachePath);
|
|
103
|
+
try {
|
|
104
|
+
if (!fs.existsSync(dir)) {
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
const cache = { lastCheck: Date.now() };
|
|
108
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache), 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Silently ignore cache write failures
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Compare two semver strings. Returns:
|
|
116
|
+
* -1 if a < b, 0 if a === b, 1 if a > b
|
|
117
|
+
*/
|
|
118
|
+
function compareSemver(a, b) {
|
|
119
|
+
const partsA = a.split('.').map(Number);
|
|
120
|
+
const partsB = b.split('.').map(Number);
|
|
121
|
+
for (let i = 0; i < 3; i++) {
|
|
122
|
+
const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
|
|
123
|
+
if (diff < 0)
|
|
124
|
+
return -1;
|
|
125
|
+
if (diff > 0)
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Non-blocking update check. Call at startup (fire-and-forget).
|
|
132
|
+
* Respects a 24-hour cooldown via a local cache file.
|
|
133
|
+
*/
|
|
134
|
+
async function checkForUpdates(currentVersion) {
|
|
135
|
+
if (!shouldCheckForUpdates())
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
const latest = await fetchLatestVersion();
|
|
139
|
+
writeCache();
|
|
140
|
+
if (compareSemver(currentVersion, latest) < 0) {
|
|
141
|
+
console.info(`\n Update available: ${currentVersion} \u2192 ${latest} \u2014 run \x1b[36mnpm i -g lazy-gravity\x1b[0m\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Silently ignore — update check should never block startup
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserMessageDetector = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
/**
|
|
7
|
+
* Script to detect the latest user message in the Antigravity chat.
|
|
8
|
+
*
|
|
9
|
+
* Antigravity user message DOM structure:
|
|
10
|
+
* <div class="bg-gray-500/15 p-2 rounded-lg w-full text-sm select-text">
|
|
11
|
+
* <div class="flex flex-row items-end gap-2">
|
|
12
|
+
* <div class="flex-1 flex flex-col gap-2">
|
|
13
|
+
* <div>
|
|
14
|
+
* <div class="whitespace-pre-wrap text-sm" style="word-break: break-word;">
|
|
15
|
+
* {user message text}
|
|
16
|
+
* </div>
|
|
17
|
+
* </div>
|
|
18
|
+
* </div>
|
|
19
|
+
* <div> <!-- undo button --> </div>
|
|
20
|
+
* </div>
|
|
21
|
+
* </div>
|
|
22
|
+
*/
|
|
23
|
+
const DETECT_USER_MESSAGE_SCRIPT = `(() => {
|
|
24
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
25
|
+
const scope = panel || document;
|
|
26
|
+
|
|
27
|
+
// Strategy A (primary): Query .whitespace-pre-wrap elements directly inside
|
|
28
|
+
// user bubble containers. This avoids the parent-container problem where
|
|
29
|
+
// querySelectorAll matches a wrapper that contains multiple bubbles.
|
|
30
|
+
const textEls = scope.querySelectorAll(
|
|
31
|
+
'[class*="bg-gray-500/15"][class*="select-text"] .whitespace-pre-wrap'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (textEls.length > 0) {
|
|
35
|
+
const lastTextEl = textEls[textEls.length - 1];
|
|
36
|
+
const text = (lastTextEl.textContent || '').trim();
|
|
37
|
+
if (text.length > 0) return { text };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Strategy B (fallback): Find individual bubble containers, filtering out
|
|
41
|
+
// any element that itself contains nested bubble elements (i.e., a parent wrapper).
|
|
42
|
+
const userBubbles = Array.from(scope.querySelectorAll(
|
|
43
|
+
'[class*="bg-gray-500/15"][class*="rounded-lg"][class*="select-text"]'
|
|
44
|
+
)).filter(el => !el.querySelector('[class*="bg-gray-500/15"][class*="select-text"]'));
|
|
45
|
+
|
|
46
|
+
if (userBubbles.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const lastBubble = userBubbles[userBubbles.length - 1];
|
|
49
|
+
const textEl = lastBubble.querySelector('.whitespace-pre-wrap')
|
|
50
|
+
|| lastBubble.querySelector('[style*="word-break"]');
|
|
51
|
+
|
|
52
|
+
const text = textEl
|
|
53
|
+
? (textEl.textContent || '').trim()
|
|
54
|
+
: (lastBubble.textContent || '').trim();
|
|
55
|
+
|
|
56
|
+
if (!text || text.length < 1) return null;
|
|
57
|
+
|
|
58
|
+
return { text };
|
|
59
|
+
})()`;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize text for echo hash comparison.
|
|
62
|
+
* Trims, collapses whitespace, and takes first 200 chars.
|
|
63
|
+
*/
|
|
64
|
+
function normalizeForHash(text) {
|
|
65
|
+
return text.trim().replace(/\s+/g, ' ').slice(0, 200);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute a short hash for echo prevention.
|
|
69
|
+
*/
|
|
70
|
+
function computeEchoHash(text) {
|
|
71
|
+
return (0, node_crypto_1.createHash)('sha256').update(normalizeForHash(text)).digest('hex').slice(0, 16);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Detects user messages posted directly in the Antigravity UI (e.g., from a PC).
|
|
75
|
+
* Follows the ApprovalDetector polling pattern.
|
|
76
|
+
*/
|
|
77
|
+
class UserMessageDetector {
|
|
78
|
+
cdpService;
|
|
79
|
+
pollIntervalMs;
|
|
80
|
+
onUserMessage;
|
|
81
|
+
pollTimer = null;
|
|
82
|
+
isRunning = false;
|
|
83
|
+
/** Hash of the last detected message (for duplicate prevention) */
|
|
84
|
+
lastDetectedHash = null;
|
|
85
|
+
/** Set of echo hashes — messages sent by LazyGravity that should be ignored */
|
|
86
|
+
echoHashes = new Set();
|
|
87
|
+
/** Set of all previously detected message hashes (defense-in-depth dedup) */
|
|
88
|
+
seenHashes = new Set();
|
|
89
|
+
static MAX_SEEN_HASHES = 50;
|
|
90
|
+
/** True during the first poll — seeds existing DOM state without firing callback */
|
|
91
|
+
isPriming = false;
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.cdpService = options.cdpService;
|
|
94
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
95
|
+
this.onUserMessage = options.onUserMessage;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register a message hash as an echo (sent by LazyGravity).
|
|
99
|
+
* When this message is detected in the DOM, it will be skipped.
|
|
100
|
+
*/
|
|
101
|
+
addEchoHash(text) {
|
|
102
|
+
const hash = computeEchoHash(text);
|
|
103
|
+
this.echoHashes.add(hash);
|
|
104
|
+
// Auto-cleanup: remove after 60s to prevent memory leak
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
this.echoHashes.delete(hash);
|
|
107
|
+
}, 60000);
|
|
108
|
+
}
|
|
109
|
+
/** Start monitoring. The first poll seeds the current DOM state without firing the callback. */
|
|
110
|
+
start() {
|
|
111
|
+
if (this.isRunning)
|
|
112
|
+
return;
|
|
113
|
+
this.isRunning = true;
|
|
114
|
+
this.lastDetectedHash = null;
|
|
115
|
+
this.seenHashes.clear();
|
|
116
|
+
this.isPriming = true;
|
|
117
|
+
// echoHashes are intentionally NOT cleared — they have their own 60s TTL
|
|
118
|
+
// and keeping them prevents false echo pickup during rapid stop/start cycles.
|
|
119
|
+
this.schedulePoll();
|
|
120
|
+
}
|
|
121
|
+
/** Stop monitoring. */
|
|
122
|
+
stop() {
|
|
123
|
+
this.isRunning = false;
|
|
124
|
+
if (this.pollTimer) {
|
|
125
|
+
clearTimeout(this.pollTimer);
|
|
126
|
+
this.pollTimer = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Returns whether monitoring is currently active. */
|
|
130
|
+
isActive() {
|
|
131
|
+
return this.isRunning;
|
|
132
|
+
}
|
|
133
|
+
/** Add a hash to the seenHashes set, evicting the oldest entry if at capacity. */
|
|
134
|
+
addToSeenHashes(hash) {
|
|
135
|
+
if (this.seenHashes.size >= UserMessageDetector.MAX_SEEN_HASHES) {
|
|
136
|
+
// Evict the oldest entry (first inserted)
|
|
137
|
+
const oldest = this.seenHashes.values().next().value;
|
|
138
|
+
if (oldest !== undefined) {
|
|
139
|
+
this.seenHashes.delete(oldest);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.seenHashes.add(hash);
|
|
143
|
+
}
|
|
144
|
+
/** Schedule the next poll. */
|
|
145
|
+
schedulePoll() {
|
|
146
|
+
if (!this.isRunning)
|
|
147
|
+
return;
|
|
148
|
+
this.pollTimer = setTimeout(async () => {
|
|
149
|
+
await this.poll();
|
|
150
|
+
if (this.isRunning) {
|
|
151
|
+
this.schedulePoll();
|
|
152
|
+
}
|
|
153
|
+
}, this.pollIntervalMs);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Single poll iteration:
|
|
157
|
+
* 1. Get latest user message from DOM
|
|
158
|
+
* 2. Check for duplicates and echoes
|
|
159
|
+
* 3. Notify via callback on new detection
|
|
160
|
+
*/
|
|
161
|
+
async poll() {
|
|
162
|
+
try {
|
|
163
|
+
const contextId = this.cdpService.getPrimaryContextId();
|
|
164
|
+
const callParams = {
|
|
165
|
+
expression: DETECT_USER_MESSAGE_SCRIPT,
|
|
166
|
+
returnByValue: true,
|
|
167
|
+
awaitPromise: false,
|
|
168
|
+
};
|
|
169
|
+
if (contextId !== null) {
|
|
170
|
+
callParams.contextId = contextId;
|
|
171
|
+
}
|
|
172
|
+
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
173
|
+
const info = result?.result?.value ?? null;
|
|
174
|
+
// Clear priming flag even if DOM is empty (e.g., new/empty chat)
|
|
175
|
+
if (this.isPriming && (!info || !info.text)) {
|
|
176
|
+
this.isPriming = false;
|
|
177
|
+
logger_1.logger.debug('[UserMessageDetector] Primed with empty DOM');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (info && info.text) {
|
|
181
|
+
const hash = computeEchoHash(info.text);
|
|
182
|
+
const preview = info.text.slice(0, 40);
|
|
183
|
+
// First poll: seed the current DOM state without firing callback
|
|
184
|
+
if (this.isPriming) {
|
|
185
|
+
this.isPriming = false;
|
|
186
|
+
this.lastDetectedHash = hash;
|
|
187
|
+
this.addToSeenHashes(hash);
|
|
188
|
+
logger_1.logger.debug(`[UserMessageDetector] Primed with existing message: "${preview}..."`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Skip if same as last detected message
|
|
192
|
+
if (hash === this.lastDetectedHash)
|
|
193
|
+
return;
|
|
194
|
+
// Skip if already seen (defense-in-depth dedup)
|
|
195
|
+
if (this.seenHashes.has(hash)) {
|
|
196
|
+
logger_1.logger.debug(`[UserMessageDetector] seenHash hit, skipping: "${preview}..."`);
|
|
197
|
+
this.lastDetectedHash = hash;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Skip if this is an echo (sent by LazyGravity)
|
|
201
|
+
if (this.echoHashes.has(hash)) {
|
|
202
|
+
logger_1.logger.debug(`[UserMessageDetector] Echo hash match, skipping: "${preview}..."`);
|
|
203
|
+
this.lastDetectedHash = hash;
|
|
204
|
+
this.addToSeenHashes(hash);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.lastDetectedHash = hash;
|
|
208
|
+
this.addToSeenHashes(hash);
|
|
209
|
+
logger_1.logger.debug(`[UserMessageDetector] New message detected: "${preview}..."`);
|
|
210
|
+
this.onUserMessage(info);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
if (message.includes('WebSocket is not connected'))
|
|
216
|
+
return;
|
|
217
|
+
logger_1.logger.error('[UserMessageDetector] Error during polling:', error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
exports.UserMessageDetector = UserMessageDetector;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.disableAllButtons = disableAllButtons;
|
|
4
|
+
const discord_js_1 = require("discord.js");
|
|
5
|
+
/**
|
|
6
|
+
* Disable all buttons in the given message component rows.
|
|
7
|
+
* Returns new ActionRows with every button set to disabled.
|
|
8
|
+
*/
|
|
9
|
+
function disableAllButtons(components) {
|
|
10
|
+
return components
|
|
11
|
+
.map((row) => {
|
|
12
|
+
const rowAny = row;
|
|
13
|
+
if (!Array.isArray(rowAny.components))
|
|
14
|
+
return null;
|
|
15
|
+
const nextRow = new discord_js_1.ActionRowBuilder();
|
|
16
|
+
const disabledButtons = rowAny.components
|
|
17
|
+
.map((component) => {
|
|
18
|
+
const componentType = component?.type ?? component?.data?.type;
|
|
19
|
+
if (componentType !== 2)
|
|
20
|
+
return null;
|
|
21
|
+
const payload = typeof component?.toJSON === 'function'
|
|
22
|
+
? component.toJSON()
|
|
23
|
+
: component;
|
|
24
|
+
return discord_js_1.ButtonBuilder.from(payload).setDisabled(true);
|
|
25
|
+
})
|
|
26
|
+
.filter((btn) => btn !== null);
|
|
27
|
+
if (disabledButtons.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
nextRow.addComponents(...disabledButtons);
|
|
30
|
+
return nextRow;
|
|
31
|
+
})
|
|
32
|
+
.filter((row) => row !== null);
|
|
33
|
+
}
|
package/dist/ui/modeUi.js
CHANGED
|
@@ -6,7 +6,17 @@ const modeService_1 = require("../services/modeService");
|
|
|
6
6
|
/**
|
|
7
7
|
* Build and send the interactive UI for the /mode command (dropdown style)
|
|
8
8
|
*/
|
|
9
|
-
async function sendModeUI(target, modeService) {
|
|
9
|
+
async function sendModeUI(target, modeService, deps) {
|
|
10
|
+
// If CDP is available, query the live mode and sync modeService
|
|
11
|
+
if (deps?.getCurrentCdp) {
|
|
12
|
+
const cdp = deps.getCurrentCdp();
|
|
13
|
+
if (cdp) {
|
|
14
|
+
const liveMode = await cdp.getCurrentMode();
|
|
15
|
+
if (liveMode) {
|
|
16
|
+
modeService.setMode(liveMode);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
const currentMode = modeService.getCurrentMode();
|
|
11
21
|
const embed = new discord_js_1.EmbedBuilder()
|
|
12
22
|
.setTitle('Mode Management')
|
package/dist/ui/modelsUi.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildModelsUI = buildModelsUI;
|
|
3
4
|
exports.sendModelsUI = sendModelsUI;
|
|
4
5
|
const discord_js_1 = require("discord.js");
|
|
5
6
|
/**
|
|
6
|
-
* Build
|
|
7
|
+
* Build the embed + button components for the models UI.
|
|
8
|
+
* Returns null when CDP is unavailable or no models are found.
|
|
7
9
|
*/
|
|
8
|
-
async function
|
|
9
|
-
const cdp = deps.getCurrentCdp();
|
|
10
|
-
if (!cdp) {
|
|
11
|
-
await target.editReply({ content: 'Not connected to CDP.' });
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
10
|
+
async function buildModelsUI(cdp, fetchQuota) {
|
|
14
11
|
const models = await cdp.getUiModels();
|
|
15
12
|
const currentModel = await cdp.getCurrentModel();
|
|
16
|
-
const quotaData = await
|
|
17
|
-
if (models.length === 0)
|
|
18
|
-
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
13
|
+
const quotaData = await fetchQuota();
|
|
14
|
+
if (models.length === 0)
|
|
15
|
+
return null;
|
|
21
16
|
function formatQuota(mName, current) {
|
|
22
17
|
if (!mName)
|
|
23
18
|
return `${current ? '[x]' : '[ ]'} Unknown`;
|
|
@@ -93,5 +88,21 @@ async function sendModelsUI(target, deps) {
|
|
|
93
88
|
rows.push(refreshRow);
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
|
-
|
|
91
|
+
return { embeds: [embed], components: rows };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build and send the interactive UI for the /models command
|
|
95
|
+
*/
|
|
96
|
+
async function sendModelsUI(target, deps) {
|
|
97
|
+
const cdp = deps.getCurrentCdp();
|
|
98
|
+
if (!cdp) {
|
|
99
|
+
await target.editReply({ content: 'Not connected to CDP.' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const payload = await buildModelsUI(cdp, deps.fetchQuota);
|
|
103
|
+
if (!payload) {
|
|
104
|
+
await target.editReply({ content: 'Failed to retrieve model list from Antigravity.' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await target.editReply({ content: '', ...payload });
|
|
97
108
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTPUT_BTN_PLAIN = exports.OUTPUT_BTN_EMBED = void 0;
|
|
4
|
+
exports.sendOutputUI = sendOutputUI;
|
|
5
|
+
const discord_js_1 = require("discord.js");
|
|
6
|
+
exports.OUTPUT_BTN_EMBED = 'output_btn_embed';
|
|
7
|
+
exports.OUTPUT_BTN_PLAIN = 'output_btn_plain';
|
|
8
|
+
async function sendOutputUI(target, currentFormat) {
|
|
9
|
+
const isEmbed = currentFormat === 'embed';
|
|
10
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
11
|
+
.setTitle('Output Format')
|
|
12
|
+
.setColor(isEmbed ? 0x5865F2 : 0x2ECC71)
|
|
13
|
+
.setDescription(`**Current Format:** ${isEmbed ? '📋 Embed' : '📝 Plain Text'}\n\n` +
|
|
14
|
+
'Embed: Rich formatting with colored borders (default).\n' +
|
|
15
|
+
'Plain Text: Simple text output, easy to copy on mobile.')
|
|
16
|
+
.setFooter({ text: 'Use buttons below to change format' })
|
|
17
|
+
.setTimestamp();
|
|
18
|
+
const row = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
|
|
19
|
+
.setCustomId(exports.OUTPUT_BTN_EMBED)
|
|
20
|
+
.setLabel('Embed')
|
|
21
|
+
.setStyle(isEmbed ? discord_js_1.ButtonStyle.Primary : discord_js_1.ButtonStyle.Secondary), new discord_js_1.ButtonBuilder()
|
|
22
|
+
.setCustomId(exports.OUTPUT_BTN_PLAIN)
|
|
23
|
+
.setLabel('Plain Text')
|
|
24
|
+
.setStyle(!isEmbed ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Secondary));
|
|
25
|
+
await target.editReply({
|
|
26
|
+
content: '',
|
|
27
|
+
embeds: [embed],
|
|
28
|
+
components: [row],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ITEMS_PER_PAGE = exports.PROJECT_PAGE_PREFIX = exports.WORKSPACE_SELECT_ID = exports.PROJECT_SELECT_ID = void 0;
|
|
4
|
+
exports.parseProjectPageId = parseProjectPageId;
|
|
5
|
+
exports.isProjectSelectId = isProjectSelectId;
|
|
6
|
+
exports.buildProjectListUI = buildProjectListUI;
|
|
7
|
+
const discord_js_1 = require("discord.js");
|
|
8
|
+
const i18n_1 = require("../utils/i18n");
|
|
9
|
+
/** Select menu custom ID (legacy, page 0) */
|
|
10
|
+
exports.PROJECT_SELECT_ID = 'project_select';
|
|
11
|
+
/** Backward compatibility: also accept old ID */
|
|
12
|
+
exports.WORKSPACE_SELECT_ID = 'workspace_select';
|
|
13
|
+
/** Button customId prefix for page navigation. Format: project_page:<page> */
|
|
14
|
+
exports.PROJECT_PAGE_PREFIX = 'project_page';
|
|
15
|
+
/** Maximum items per page (Discord select menu limit) */
|
|
16
|
+
exports.ITEMS_PER_PAGE = 25;
|
|
17
|
+
/**
|
|
18
|
+
* Parse page number from a page-button customId.
|
|
19
|
+
* Returns NaN if the customId does not match the expected format.
|
|
20
|
+
*/
|
|
21
|
+
function parseProjectPageId(customId) {
|
|
22
|
+
if (!customId.startsWith(`${exports.PROJECT_PAGE_PREFIX}:`))
|
|
23
|
+
return NaN;
|
|
24
|
+
return parseInt(customId.slice(exports.PROJECT_PAGE_PREFIX.length + 1), 10);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a customId belongs to a project select menu.
|
|
28
|
+
* Matches legacy IDs (`project_select`, `workspace_select`) and
|
|
29
|
+
* paginated IDs (`project_select:<page>`).
|
|
30
|
+
*/
|
|
31
|
+
function isProjectSelectId(customId) {
|
|
32
|
+
return (customId === exports.PROJECT_SELECT_ID ||
|
|
33
|
+
customId === exports.WORKSPACE_SELECT_ID ||
|
|
34
|
+
customId.startsWith(`${exports.PROJECT_SELECT_ID}:`));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the project list UI with select menu and optional Prev/Next buttons.
|
|
38
|
+
*
|
|
39
|
+
* @param workspaces - Full list of workspace names
|
|
40
|
+
* @param page - Zero-based page index (clamped to valid range)
|
|
41
|
+
* @returns Object with embeds and components arrays ready for Discord reply
|
|
42
|
+
*/
|
|
43
|
+
function buildProjectListUI(workspaces, page = 0) {
|
|
44
|
+
const totalPages = Math.max(1, Math.ceil(workspaces.length / exports.ITEMS_PER_PAGE));
|
|
45
|
+
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
46
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
47
|
+
.setTitle('\u{1F4C1} Projects')
|
|
48
|
+
.setColor(0x5865F2)
|
|
49
|
+
.setDescription((0, i18n_1.t)('Select a project to auto-create a category and session channel'))
|
|
50
|
+
.setTimestamp();
|
|
51
|
+
if (workspaces.length === 0) {
|
|
52
|
+
return { embeds: [embed], components: [] };
|
|
53
|
+
}
|
|
54
|
+
const start = safePage * exports.ITEMS_PER_PAGE;
|
|
55
|
+
const end = Math.min(start + exports.ITEMS_PER_PAGE, workspaces.length);
|
|
56
|
+
const pageItems = workspaces.slice(start, end);
|
|
57
|
+
if (totalPages > 1) {
|
|
58
|
+
embed.setFooter({
|
|
59
|
+
text: `Page ${safePage + 1} / ${totalPages} (${workspaces.length} projects total)`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const components = [];
|
|
63
|
+
const options = pageItems.map((ws) => ({ label: ws, value: ws }));
|
|
64
|
+
const selectMenu = new discord_js_1.StringSelectMenuBuilder()
|
|
65
|
+
.setCustomId(`${exports.PROJECT_SELECT_ID}:${safePage}`)
|
|
66
|
+
.setPlaceholder((0, i18n_1.t)('Select a project...'))
|
|
67
|
+
.addOptions(options);
|
|
68
|
+
components.push(new discord_js_1.ActionRowBuilder().addComponents(selectMenu));
|
|
69
|
+
if (totalPages > 1) {
|
|
70
|
+
const prevBtn = new discord_js_1.ButtonBuilder()
|
|
71
|
+
.setCustomId(`${exports.PROJECT_PAGE_PREFIX}:${Math.max(0, safePage - 1)}`)
|
|
72
|
+
.setLabel('\u25C0 Prev')
|
|
73
|
+
.setStyle(discord_js_1.ButtonStyle.Secondary)
|
|
74
|
+
.setDisabled(safePage === 0);
|
|
75
|
+
const nextBtn = new discord_js_1.ButtonBuilder()
|
|
76
|
+
.setCustomId(`${exports.PROJECT_PAGE_PREFIX}:${safePage + 1}`)
|
|
77
|
+
.setLabel('Next \u25B6')
|
|
78
|
+
.setStyle(discord_js_1.ButtonStyle.Secondary)
|
|
79
|
+
.setDisabled(safePage >= totalPages - 1);
|
|
80
|
+
components.push(new discord_js_1.ActionRowBuilder().addComponents(prevBtn, nextBtn));
|
|
81
|
+
}
|
|
82
|
+
return { embeds: [embed], components };
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SESSION_SELECT_ID = void 0;
|
|
4
|
+
exports.isSessionSelectId = isSessionSelectId;
|
|
5
|
+
exports.buildSessionPickerUI = buildSessionPickerUI;
|
|
6
|
+
const discord_js_1 = require("discord.js");
|
|
7
|
+
const i18n_1 = require("../utils/i18n");
|
|
8
|
+
/** Select menu custom ID for session picker */
|
|
9
|
+
exports.SESSION_SELECT_ID = 'session_select';
|
|
10
|
+
/** Maximum items per select menu (Discord limit) */
|
|
11
|
+
const MAX_SELECT_OPTIONS = 25;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a customId belongs to the session select menu.
|
|
14
|
+
*/
|
|
15
|
+
function isSessionSelectId(customId) {
|
|
16
|
+
return customId === exports.SESSION_SELECT_ID;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the session picker UI with a select menu.
|
|
20
|
+
*
|
|
21
|
+
* @param sessions - List of sessions from the side panel
|
|
22
|
+
* @returns Object with embeds and components arrays ready for Discord reply
|
|
23
|
+
*/
|
|
24
|
+
function buildSessionPickerUI(sessions) {
|
|
25
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
26
|
+
.setTitle((0, i18n_1.t)('🔗 Join Session'))
|
|
27
|
+
.setColor(0x5865F2)
|
|
28
|
+
.setTimestamp();
|
|
29
|
+
if (sessions.length === 0) {
|
|
30
|
+
embed.setDescription((0, i18n_1.t)('No sessions found in the Antigravity side panel.'));
|
|
31
|
+
return { embeds: [embed], components: [] };
|
|
32
|
+
}
|
|
33
|
+
embed.setDescription((0, i18n_1.t)(`Select a session to join (${sessions.length} found)`));
|
|
34
|
+
const pageItems = sessions.slice(0, MAX_SELECT_OPTIONS);
|
|
35
|
+
const options = pageItems.map((session) => ({
|
|
36
|
+
label: session.title.slice(0, 100),
|
|
37
|
+
value: session.title.slice(0, 100),
|
|
38
|
+
description: session.isActive ? (0, i18n_1.t)('Current') : undefined,
|
|
39
|
+
}));
|
|
40
|
+
const selectMenu = new discord_js_1.StringSelectMenuBuilder()
|
|
41
|
+
.setCustomId(exports.SESSION_SELECT_ID)
|
|
42
|
+
.setPlaceholder((0, i18n_1.t)('Select a session...'))
|
|
43
|
+
.addOptions(options);
|
|
44
|
+
const components = [
|
|
45
|
+
new discord_js_1.ActionRowBuilder().addComponents(selectMenu),
|
|
46
|
+
];
|
|
47
|
+
return { embeds: [embed], components };
|
|
48
|
+
}
|