@quantiya/codevibe-antigravity-plugin 2.0.10 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +257 -42
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -1121,13 +1121,16 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1121
1121
|
if (kind === "question") {
|
|
1122
1122
|
const q = extractQuestionHeader(snapshot);
|
|
1123
1123
|
headerText = q ? q.body : null;
|
|
1124
|
+
} else if (kind === "file_access") {
|
|
1125
|
+
const fileAccess = extractFileAccessPrompt(snapshot);
|
|
1126
|
+
headerText = fileAccess ? fileAccess.identity : null;
|
|
1124
1127
|
} else if (kind === "approval") {
|
|
1125
1128
|
headerText = extractHeader(snapshot);
|
|
1126
1129
|
}
|
|
1127
1130
|
}
|
|
1128
|
-
return { active, headerText };
|
|
1131
|
+
return { active, headerText, probeSucceeded: true };
|
|
1129
1132
|
} catch {
|
|
1130
|
-
return { active: false, headerText: null };
|
|
1133
|
+
return { active: false, headerText: null, probeSucceeded: false };
|
|
1131
1134
|
}
|
|
1132
1135
|
}
|
|
1133
1136
|
// ─── tmux pipe-pane plumbing ─────────────────────────────────────────────
|
|
@@ -1388,7 +1391,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
|
|
|
1388
1391
|
function looksLikePromptDelta(chunk) {
|
|
1389
1392
|
return (
|
|
1390
1393
|
// Approval UI signatures
|
|
1391
|
-
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(chunk) || /^\s*[1-9]\. (?:Yes|No)\b/im.test(chunk) || /\[(?:y\/n|Y\/n|y\/N)\]/.test(chunk) || /Question \d+\/\d+:/i.test(chunk) || /\benter Select\b/i.test(chunk)
|
|
1394
|
+
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(chunk) || /^\s*[1-9]\. (?:Yes|No)\b/im.test(chunk) || /\[(?:y\/n|Y\/n|y\/N)\]/.test(chunk) || /Question \d+\/\d+:/i.test(chunk) || /\benter Select\b/i.test(chunk) || /^File access\s*$/im.test(chunk) || /Allow access to this file\?/i.test(chunk) || /No, deny access/i.test(chunk)
|
|
1392
1395
|
);
|
|
1393
1396
|
}
|
|
1394
1397
|
function looksLikePromptSnapshot(snapshot) {
|
|
@@ -1402,8 +1405,31 @@ function looksLikePromptSnapshot(snapshot) {
|
|
|
1402
1405
|
const hasQuestionOptions = /^[>\s]+[1-9]\. \S/im.test(recent);
|
|
1403
1406
|
const hasQuestionFooter = /\benter Select\b/i.test(recent);
|
|
1404
1407
|
const isQuestion = hasQuestionHeader && hasQuestionOptions && hasQuestionFooter;
|
|
1408
|
+
const hasFileAccessHeader = /^File access\s*$/im.test(recent);
|
|
1409
|
+
const hasFileAccessQuestion = /Allow access to this file\?/i.test(recent);
|
|
1410
|
+
const isFileAccess = hasFileAccessHeader && hasFileAccessQuestion && hasApprovalOptions && hasApprovalFooter;
|
|
1405
1411
|
const legacyYN = /\[(?:y\/n|Y\/n|y\/N)\]/.test(recent) && /\b(?:apply|approve|allow|continue|proceed|run|execute|confirm)\b/i.test(recent);
|
|
1406
|
-
return legacyYN || isApproval || isQuestion;
|
|
1412
|
+
return legacyYN || isApproval || isQuestion || isFileAccess;
|
|
1413
|
+
}
|
|
1414
|
+
function extractFileAccessPrompt(snapshot) {
|
|
1415
|
+
const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
|
|
1416
|
+
const lines = stripped.split("\n");
|
|
1417
|
+
let headingIndex = -1;
|
|
1418
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1419
|
+
if (/^\s*File access\s*$/i.test(lines[index])) {
|
|
1420
|
+
headingIndex = index;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (headingIndex < 0) return null;
|
|
1425
|
+
const block = lines.slice(headingIndex, headingIndex + 20);
|
|
1426
|
+
const questionIndex = block.findIndex((line) => /Allow access to this file\?/i.test(line));
|
|
1427
|
+
if (questionIndex < 0) return null;
|
|
1428
|
+
const readLine = block.slice(0, questionIndex + 1).find((line) => /^\s*Read:\s*\S/i.test(line));
|
|
1429
|
+
const identity = readLine?.replace(/^\s*Read:\s*/i, "").trim() || "non-workspace file access";
|
|
1430
|
+
const fullLine = block[questionIndex].trim();
|
|
1431
|
+
const body = block.slice(1, questionIndex).join("\n").trim();
|
|
1432
|
+
return { identity, fullLine, body };
|
|
1407
1433
|
}
|
|
1408
1434
|
function extractHeader(snapshot) {
|
|
1409
1435
|
const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
|
|
@@ -1422,8 +1448,10 @@ function detectActivePromptKind(snapshot) {
|
|
|
1422
1448
|
const lines = stripped.split("\n");
|
|
1423
1449
|
const approvalRe = /Requesting permission for:/i;
|
|
1424
1450
|
const questionRe = /Question \d+\/\d+:/i;
|
|
1451
|
+
const fileAccessRe = /^\s*File access\s*$/i;
|
|
1425
1452
|
let lastApprovalLineIdx = -1;
|
|
1426
1453
|
let lastQuestionLineIdx = -1;
|
|
1454
|
+
let lastFileAccessLineIdx = -1;
|
|
1427
1455
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1428
1456
|
if (lastApprovalLineIdx < 0 && approvalRe.test(lines[i])) {
|
|
1429
1457
|
lastApprovalLineIdx = i;
|
|
@@ -1431,10 +1459,15 @@ function detectActivePromptKind(snapshot) {
|
|
|
1431
1459
|
if (lastQuestionLineIdx < 0 && questionRe.test(lines[i])) {
|
|
1432
1460
|
lastQuestionLineIdx = i;
|
|
1433
1461
|
}
|
|
1434
|
-
if (
|
|
1462
|
+
if (lastFileAccessLineIdx < 0 && fileAccessRe.test(lines[i])) {
|
|
1463
|
+
lastFileAccessLineIdx = i;
|
|
1464
|
+
}
|
|
1465
|
+
if (lastApprovalLineIdx >= 0 && lastQuestionLineIdx >= 0 && lastFileAccessLineIdx >= 0) break;
|
|
1435
1466
|
}
|
|
1436
|
-
|
|
1437
|
-
if (
|
|
1467
|
+
const latest = Math.max(lastApprovalLineIdx, lastQuestionLineIdx, lastFileAccessLineIdx);
|
|
1468
|
+
if (latest < 0) return null;
|
|
1469
|
+
if (latest === lastFileAccessLineIdx) return "file_access";
|
|
1470
|
+
if (latest === lastQuestionLineIdx) return "question";
|
|
1438
1471
|
return "approval";
|
|
1439
1472
|
}
|
|
1440
1473
|
function extractQuestionHeader(snapshot) {
|
|
@@ -1468,10 +1501,17 @@ var import_events3 = require("events");
|
|
|
1468
1501
|
var import_uuid = require("uuid");
|
|
1469
1502
|
|
|
1470
1503
|
// src/prompt-parser.ts
|
|
1504
|
+
var import_crypto2 = require("crypto");
|
|
1471
1505
|
function parseApprovalSnapshot(snapshot) {
|
|
1472
1506
|
if (!snapshot) return null;
|
|
1473
1507
|
const stripped = stripAnsi(snapshot);
|
|
1474
1508
|
const kind = detectActivePromptKind(stripped);
|
|
1509
|
+
if (kind === "file_access") {
|
|
1510
|
+
const fileAccess = extractFileAccessPrompt(stripped);
|
|
1511
|
+
if (fileAccess) {
|
|
1512
|
+
return parseFileAccessSnapshot(stripped, snapshot, fileAccess);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1475
1515
|
if (kind === "question") {
|
|
1476
1516
|
const questionHeader = extractQuestionHeader(stripped);
|
|
1477
1517
|
if (questionHeader) {
|
|
@@ -1480,6 +1520,49 @@ function parseApprovalSnapshot(snapshot) {
|
|
|
1480
1520
|
}
|
|
1481
1521
|
return parseApprovalUISnapshot(stripped, snapshot);
|
|
1482
1522
|
}
|
|
1523
|
+
function buildApprovalSemanticKey(candidate) {
|
|
1524
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
1525
|
+
const payload = {
|
|
1526
|
+
kind: candidate.kind,
|
|
1527
|
+
headerText: normalize(candidate.headerText),
|
|
1528
|
+
body: normalize(candidate.body),
|
|
1529
|
+
options: candidate.options.map((option) => ({
|
|
1530
|
+
number: option.number,
|
|
1531
|
+
text: normalize(option.text)
|
|
1532
|
+
})),
|
|
1533
|
+
submitMap: Object.fromEntries(
|
|
1534
|
+
Object.entries(candidate.submitMap).sort(([left], [right]) => Number(left) - Number(right)).map(([number, keys]) => [number, [...keys]])
|
|
1535
|
+
)
|
|
1536
|
+
};
|
|
1537
|
+
return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1538
|
+
}
|
|
1539
|
+
function hasNegativeApprovalOption(options) {
|
|
1540
|
+
return options.some((option) => /^(?:no|deny|reject|cancel)\b/i.test(option.text.trim()));
|
|
1541
|
+
}
|
|
1542
|
+
function parseFileAccessSnapshot(stripped, originalSnapshot, prompt) {
|
|
1543
|
+
const lines = stripped.split("\n");
|
|
1544
|
+
let questionIndex = -1;
|
|
1545
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1546
|
+
if (/Allow access to this file\?/i.test(lines[index])) {
|
|
1547
|
+
questionIndex = index;
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const belowQuestion = questionIndex >= 0 ? lines.slice(questionIndex + 1).join("\n") : stripped;
|
|
1552
|
+
const options = parseOptions(belowQuestion);
|
|
1553
|
+
if (options.length < 2 || !hasNegativeApprovalOption(options)) return null;
|
|
1554
|
+
return {
|
|
1555
|
+
kind: "approval",
|
|
1556
|
+
headerText: prompt.identity,
|
|
1557
|
+
fullHeaderLine: prompt.fullLine,
|
|
1558
|
+
command: void 0,
|
|
1559
|
+
filePath: prompt.identity === "non-workspace file access" ? void 0 : prompt.identity,
|
|
1560
|
+
options,
|
|
1561
|
+
submitMap: buildApprovalSubmitMap(options),
|
|
1562
|
+
body: prompt.body,
|
|
1563
|
+
paneHash: hashPromptSnapshot(originalSnapshot)
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1483
1566
|
function parseApprovalUISnapshot(stripped, originalSnapshot) {
|
|
1484
1567
|
const headerText = extractHeader(stripped);
|
|
1485
1568
|
if (!headerText) return null;
|
|
@@ -1494,7 +1577,7 @@ function parseApprovalUISnapshot(stripped, originalSnapshot) {
|
|
|
1494
1577
|
}
|
|
1495
1578
|
const belowHeader = headerIdx >= 0 ? lines.slice(headerIdx + 1).join("\n") : stripped;
|
|
1496
1579
|
const options = parseOptions(belowHeader);
|
|
1497
|
-
if (options.length < 2) return null;
|
|
1580
|
+
if (options.length < 2 || !hasNegativeApprovalOption(options)) return null;
|
|
1498
1581
|
const submitMap = buildApprovalSubmitMap(options);
|
|
1499
1582
|
const { command, filePath } = extractIdentity(headerText);
|
|
1500
1583
|
const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
|
|
@@ -1785,9 +1868,10 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
1785
1868
|
matchedPaneHeader: candidate.headerText,
|
|
1786
1869
|
paneDisplayHeader: candidate.fullHeaderLine,
|
|
1787
1870
|
body: candidate.body,
|
|
1871
|
+
paneOptions: candidate.options,
|
|
1872
|
+
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
1788
1873
|
emittedAt: Date.now(),
|
|
1789
1874
|
ttlMs: this.promptTtlMs,
|
|
1790
|
-
paneOptions: candidate.options,
|
|
1791
1875
|
// E1 (§6a-2b / F5) — carry the consumed pane hash so an emit-failure
|
|
1792
1876
|
// rollback can reset the observer's lastPromptHash and let the unchanged
|
|
1793
1877
|
// live pane re-emit.
|
|
@@ -1974,6 +2058,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
1974
2058
|
matchedPaneHeader: candidate.headerText,
|
|
1975
2059
|
paneDisplayHeader: candidate.fullHeaderLine,
|
|
1976
2060
|
body: candidate.body,
|
|
2061
|
+
paneOptions: candidate.options,
|
|
2062
|
+
paneSemanticKey: buildApprovalSemanticKey(candidate),
|
|
1977
2063
|
emittedAt: Date.now(),
|
|
1978
2064
|
ttlMs: this.promptTtlMs
|
|
1979
2065
|
};
|
|
@@ -2054,6 +2140,10 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
2054
2140
|
getPendingPrompt(promptId) {
|
|
2055
2141
|
return this.pendingPrompts.get(promptId) ?? null;
|
|
2056
2142
|
}
|
|
2143
|
+
getPendingPromptsForConversation(conversationId) {
|
|
2144
|
+
const pending = Array.from(this.pendingPrompts.values());
|
|
2145
|
+
return conversationId ? pending.filter((state) => state.conversationId === conversationId) : pending;
|
|
2146
|
+
}
|
|
2057
2147
|
// ─── Read-only accessors ────────────────────────────────────────────────
|
|
2058
2148
|
getPendingCalls() {
|
|
2059
2149
|
return Array.from(this.pendingCalls.values());
|
|
@@ -2107,21 +2197,6 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
2107
2197
|
this.emit("pending-call-expired", call);
|
|
2108
2198
|
}
|
|
2109
2199
|
}
|
|
2110
|
-
let anyExpired = false;
|
|
2111
|
-
for (const [promptId, state] of this.pendingPrompts.entries()) {
|
|
2112
|
-
if (now - state.emittedAt > state.ttlMs) {
|
|
2113
|
-
this.pendingPrompts.delete(promptId);
|
|
2114
|
-
this.resolvedPrompts.set(promptId, now);
|
|
2115
|
-
anyExpired = true;
|
|
2116
|
-
logger.debug("Pending prompt expired (TTL)", {
|
|
2117
|
-
promptId,
|
|
2118
|
-
ageMs: now - state.emittedAt
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
if (anyExpired) {
|
|
2123
|
-
this.paneOnlyEmittedHashes.clear();
|
|
2124
|
-
}
|
|
2125
2200
|
for (const [promptId, resolvedAt] of this.resolvedPrompts.entries()) {
|
|
2126
2201
|
if (now - resolvedAt > 5 * 6e4) {
|
|
2127
2202
|
this.resolvedPrompts.delete(promptId);
|
|
@@ -2228,22 +2303,11 @@ var PromptResponder = class {
|
|
|
2228
2303
|
};
|
|
2229
2304
|
}
|
|
2230
2305
|
if (this.paneObserver) {
|
|
2231
|
-
|
|
2232
|
-
if (!probe.active) {
|
|
2233
|
-
this.detector.resolvePrompt(promptId);
|
|
2234
|
-
return {
|
|
2235
|
-
ok: false,
|
|
2236
|
-
reason: "prompt-superseded",
|
|
2237
|
-
details: "approval UI vanished from pane (likely user approved on desktop)"
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
const expectedHeader = state.matchedPaneHeader;
|
|
2241
|
-
if (expectedHeader && probe.headerText && !sameIdentity(probe.headerText, expectedHeader)) {
|
|
2242
|
-
this.detector.resolvePrompt(promptId);
|
|
2306
|
+
if (!state.paneSemanticKey) {
|
|
2243
2307
|
return {
|
|
2244
2308
|
ok: false,
|
|
2245
|
-
reason: "prompt-
|
|
2246
|
-
details:
|
|
2309
|
+
reason: "prompt-probe-failed",
|
|
2310
|
+
details: "pending prompt has no exact semantic identity"
|
|
2247
2311
|
};
|
|
2248
2312
|
}
|
|
2249
2313
|
}
|
|
@@ -2257,6 +2321,51 @@ var PromptResponder = class {
|
|
|
2257
2321
|
}
|
|
2258
2322
|
try {
|
|
2259
2323
|
for (const key of keys) {
|
|
2324
|
+
if (!this.detector || !this.paneObserver) {
|
|
2325
|
+
return {
|
|
2326
|
+
ok: false,
|
|
2327
|
+
reason: "prompt-probe-failed",
|
|
2328
|
+
details: "approval responder is missing its detector or pane observer"
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
if (this.detector.getPendingPrompt(promptId) !== state) {
|
|
2332
|
+
return { ok: false, reason: "prompt-expired" };
|
|
2333
|
+
}
|
|
2334
|
+
let liveSemanticKey = null;
|
|
2335
|
+
try {
|
|
2336
|
+
const snapshot = await this.paneObserver.captureSnapshot();
|
|
2337
|
+
const live = parseApprovalSnapshot(snapshot);
|
|
2338
|
+
if (!live) {
|
|
2339
|
+
if (detectActivePromptKind(snapshot) === null) {
|
|
2340
|
+
this.detector.resolvePrompt(promptId);
|
|
2341
|
+
return {
|
|
2342
|
+
ok: false,
|
|
2343
|
+
reason: "prompt-superseded",
|
|
2344
|
+
details: "the approval chooser is no longer active"
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
ok: false,
|
|
2349
|
+
reason: "prompt-probe-failed",
|
|
2350
|
+
details: "the active chooser could not be parsed exactly"
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
return {
|
|
2356
|
+
ok: false,
|
|
2357
|
+
reason: "prompt-probe-failed",
|
|
2358
|
+
details: error instanceof Error ? error.message : String(error)
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
if (liveSemanticKey !== state.paneSemanticKey) {
|
|
2362
|
+
this.detector.resolvePrompt(promptId);
|
|
2363
|
+
return {
|
|
2364
|
+
ok: false,
|
|
2365
|
+
reason: "prompt-superseded",
|
|
2366
|
+
details: "active chooser semantics differ from the mobile prompt"
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2260
2369
|
if (isNamedKey(key)) {
|
|
2261
2370
|
await this.sendKey(target, key);
|
|
2262
2371
|
} else {
|
|
@@ -2289,6 +2398,43 @@ var PromptResponder = class {
|
|
|
2289
2398
|
};
|
|
2290
2399
|
}
|
|
2291
2400
|
}
|
|
2401
|
+
async sendFreeFormWhenNoApprovalActive(text) {
|
|
2402
|
+
const target = this.resolveTmuxTarget();
|
|
2403
|
+
if (!target) return { ok: false, reason: "no-tmux-target" };
|
|
2404
|
+
if (!this.paneObserver) {
|
|
2405
|
+
return {
|
|
2406
|
+
ok: false,
|
|
2407
|
+
reason: "prompt-probe-failed",
|
|
2408
|
+
details: "pane observer is required for safe free-form input"
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const chooserIsAbsent = async () => {
|
|
2412
|
+
const probe = await this.paneObserver.probeApprovalUIActive();
|
|
2413
|
+
if (!probe.probeSucceeded) {
|
|
2414
|
+
return { ok: false, reason: "prompt-probe-failed" };
|
|
2415
|
+
}
|
|
2416
|
+
if (probe.active) {
|
|
2417
|
+
return { ok: false, reason: "prompt-superseded" };
|
|
2418
|
+
}
|
|
2419
|
+
return null;
|
|
2420
|
+
};
|
|
2421
|
+
try {
|
|
2422
|
+
const beforeText = await chooserIsAbsent();
|
|
2423
|
+
if (beforeText) return beforeText;
|
|
2424
|
+
await this.typeLiteral(target, text);
|
|
2425
|
+
await delay(ENTER_DELAY_MS);
|
|
2426
|
+
const beforeEnter = await chooserIsAbsent();
|
|
2427
|
+
if (beforeEnter) return beforeEnter;
|
|
2428
|
+
await this.sendKey(target, "Enter");
|
|
2429
|
+
return { ok: true };
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
return {
|
|
2432
|
+
ok: false,
|
|
2433
|
+
reason: "tmux-failed",
|
|
2434
|
+
details: error instanceof Error ? error.message : String(error)
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2292
2438
|
// ─── Tmux primitives ────────────────────────────────────────────────────
|
|
2293
2439
|
async typeLiteral(target, text) {
|
|
2294
2440
|
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
@@ -2314,9 +2460,6 @@ function isNamedKey(key) {
|
|
|
2314
2460
|
function delay(ms) {
|
|
2315
2461
|
return new Promise((r) => setTimeout(r, ms));
|
|
2316
2462
|
}
|
|
2317
|
-
function sameIdentity(headerFromPane, expected) {
|
|
2318
|
-
return headerFromPane.trim() === expected.trim();
|
|
2319
|
-
}
|
|
2320
2463
|
|
|
2321
2464
|
// src/mobile-prompt-dedupe.ts
|
|
2322
2465
|
var DEFAULT_EXPIRY_MS = 15e3;
|
|
@@ -4242,8 +4385,63 @@ var McpServer = class _McpServer {
|
|
|
4242
4385
|
});
|
|
4243
4386
|
}
|
|
4244
4387
|
}
|
|
4388
|
+
const pendingPrompts = this.approvalDetector.getPendingPromptsForConversation(
|
|
4389
|
+
session.conversationId
|
|
4390
|
+
);
|
|
4391
|
+
if (pendingPrompts.length === 0) {
|
|
4392
|
+
const probe = await this.paneObserver.probeApprovalUIActive();
|
|
4393
|
+
if (!probe.probeSucceeded) {
|
|
4394
|
+
await this.emitPromptSafetyNotification(
|
|
4395
|
+
session,
|
|
4396
|
+
"Your message was not sent because the desktop approval state could not be verified. Try again or resolve the prompt on the desktop."
|
|
4397
|
+
);
|
|
4398
|
+
return;
|
|
4399
|
+
}
|
|
4400
|
+
if (probe.active) {
|
|
4401
|
+
await this.emitPromptSafetyNotification(
|
|
4402
|
+
session,
|
|
4403
|
+
"Your message was not sent because a desktop approval is active but its options are not yet available on mobile. Resolve it on the desktop and send the message again."
|
|
4404
|
+
);
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
if (pendingPrompts.length > 0) {
|
|
4409
|
+
if (pendingPrompts.length !== 1) {
|
|
4410
|
+
await this.emitPromptSafetyNotification(
|
|
4411
|
+
session,
|
|
4412
|
+
"Your message was not sent because more than one desktop approval is pending. Resolve the approvals and send it again."
|
|
4413
|
+
);
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
const activePrompt = pendingPrompts[0];
|
|
4417
|
+
const rejectOption = activePrompt.paneOptions?.find(
|
|
4418
|
+
(option) => /^(?:no|deny|reject|cancel)\b/i.test(option.text.trim()) && (activePrompt.submitMap[option.number]?.length ?? 0) > 0
|
|
4419
|
+
);
|
|
4420
|
+
if (!rejectOption) {
|
|
4421
|
+
await this.emitPromptSafetyNotification(
|
|
4422
|
+
session,
|
|
4423
|
+
"Your message was not sent because the active desktop approval has no verified reject option. Resolve it on the desktop and send the message again."
|
|
4424
|
+
);
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4427
|
+
this.mobileDeduper.track(session.sessionId, rejectOption.number);
|
|
4428
|
+
const rejectResult = await this.promptResponder.sendApprovalReply(
|
|
4429
|
+
activePrompt.promptId,
|
|
4430
|
+
rejectOption.number
|
|
4431
|
+
);
|
|
4432
|
+
if (!rejectResult.ok) {
|
|
4433
|
+
this.mobileDeduper.forget(session.sessionId, rejectOption.number);
|
|
4434
|
+
logger.warn("Reject-before-free-form failed", {
|
|
4435
|
+
promptId: activePrompt.promptId,
|
|
4436
|
+
reason: rejectResult.reason,
|
|
4437
|
+
details: rejectResult.details
|
|
4438
|
+
});
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
await new Promise((resolve3) => setTimeout(resolve3, 250));
|
|
4442
|
+
}
|
|
4245
4443
|
this.mobileDeduper.track(session.sessionId, promptContent);
|
|
4246
|
-
const result = await this.promptResponder.
|
|
4444
|
+
const result = await this.promptResponder.sendFreeFormWhenNoApprovalActive(promptContent);
|
|
4247
4445
|
if (!result.ok) {
|
|
4248
4446
|
this.mobileDeduper.forget(session.sessionId, promptContent);
|
|
4249
4447
|
logger.warn("sendFreeForm failed", { reason: result.reason, details: result.details });
|
|
@@ -4278,6 +4476,23 @@ var McpServer = class _McpServer {
|
|
|
4278
4476
|
await this.markExecuted(evt);
|
|
4279
4477
|
}
|
|
4280
4478
|
}
|
|
4479
|
+
async emitPromptSafetyNotification(session, content) {
|
|
4480
|
+
const input = {
|
|
4481
|
+
sessionId: session.sessionId,
|
|
4482
|
+
type: import_codevibe_core4.EventType.NOTIFICATION,
|
|
4483
|
+
source: import_codevibe_core4.EventSource.DESKTOP,
|
|
4484
|
+
content,
|
|
4485
|
+
metadata: { promptSafetyBlocked: true },
|
|
4486
|
+
timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: session.sessionId })
|
|
4487
|
+
};
|
|
4488
|
+
const outbound = this.encryptOutbound(session, input);
|
|
4489
|
+
if (!outbound) return;
|
|
4490
|
+
try {
|
|
4491
|
+
await this.appSyncClient.createEvent(outbound);
|
|
4492
|
+
} catch (error) {
|
|
4493
|
+
logger.warn("Failed to emit prompt-safety notification", { error: String(error) });
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4281
4496
|
/**
|
|
4282
4497
|
* Transition a mobile event's deliveryStatus to DELIVERED. Called as
|
|
4283
4498
|
* soon as the plugin receives + decrypts the event and BEFORE the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-antigravity-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"codevibe": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"node": ">=22.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@quantiya/codevibe-core": "2.0.
|
|
51
|
+
"@quantiya/codevibe-core": "2.0.10",
|
|
52
52
|
"chokidar": "^5.0.0",
|
|
53
53
|
"dotenv": "^16.6.1",
|
|
54
54
|
"express": "^5.1.0",
|