@quantiya/codevibe-antigravity-plugin 2.0.85 → 2.0.86

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.
Files changed (2) hide show
  1. package/dist/server.js +409 -90
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -32,6 +32,7 @@ var server_exports = {};
32
32
  __export(server_exports, {
33
33
  ApprovalDetector: () => ApprovalDetector,
34
34
  McpServer: () => McpServer,
35
+ PromptResponder: () => PromptResponder,
35
36
  SessionNotFoundError: () => SessionNotFoundError,
36
37
  StoredAuthFailureError: () => StoredAuthFailureError,
37
38
  TERMINAL_SHUTDOWN_SIGNALS: () => TERMINAL_SHUTDOWN_SIGNALS,
@@ -2924,6 +2925,7 @@ function isComposerBoxAt(lines, idx) {
2924
2925
  }
2925
2926
  return false;
2926
2927
  }
2928
+ var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
2927
2929
  function isPaneInputSafetyBlocked(input) {
2928
2930
  const rawText = Array.isArray(input) ? input.join("\n") : input;
2929
2931
  if (!rawText) return { blocked: false, kind: null };
@@ -3093,9 +3095,10 @@ function isPaneInputSafetyBlocked(input) {
3093
3095
  return { blocked: false, kind: null };
3094
3096
  }
3095
3097
  function looksLikePromptDelta(chunk) {
3098
+ const text = chunk.includes("\x1B") ? chunk.replace(ANSI_ESCAPE_REGEX, "") : chunk;
3096
3099
  return (
3097
3100
  // Approval UI signatures
3098
- /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) || /^\s*(?:[❯>]\s*)?\?\s+(?!for\s+(?:shortcuts|help|commands))\S/im.test(chunk) || /Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:/i.test(chunk) || /\benter\s+(?:to\s+)?select\b/i.test(chunk) || /\b(?:navigate|use\s+arrow\s+keys)\b/i.test(chunk) || /^File access\s*$/im.test(chunk) || /^\s*Allow access to this file\?\s*$/im.test(chunk) || /(?:Yes, allow access|No, deny access)/i.test(chunk) || /(?:↑\/↓\s+Navigate|ctrl\+r\s+Review)/i.test(chunk) || /Our servers are experiencing/i.test(chunk) || /Error ID:\s*[0-9a-fA-F-]*/i.test(chunk) || /(?:⚠️|⚠\uFE0F?)\s+.*(?:high[- ]traffic|try again in a minute|try again later|stream was interrupted|stream interrupted|overloaded|unexpected error|quota\b|upgrade your subscription|increase your limits|resets in\s+\d|credits balance is too low|out of credits|rate[- ]limit|capacity exhausted|exhausted your quota|internal error|currently unreachable|network issue connecting|authentication expired|authentication issue|terminated due to error|invalid output|invalid tool call)/i.test(chunk)
3101
+ /Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(text) || /^\s*[1-9]\. (?:Yes|No)\b/im.test(text) || /\[(?:y\/n|Y\/n|y\/N)\]/.test(chunk) || /^\s*(?:[❯>]\s*)?\?\s+(?!for\s+(?:shortcuts|help|commands))\S/im.test(text) || /Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:/i.test(text) || /\benter\s+(?:to\s+)?(?:select|submit(?:\s+all)?|next)\b/i.test(text) || /\b(?:navigate|use\s+arrow\s+keys)\b/i.test(text) || /\b\d+[\/:of\s]+\d+:\s+\S/i.test(text) || /\bSubmit(?:\s+All)?\b/i.test(text) || /(?:^|[\s·|│])(?:←|<-)\s*Back\b/i.test(text) || /^File access\s*$/im.test(text) || /^\s*Allow access to this file\?\s*$/im.test(text) || /(?:Yes, allow access|No, deny access)/i.test(text) || /(?:↑\/↓\s+Navigate|ctrl\+r\s+Review)/i.test(text) || /Our servers are experiencing/i.test(text) || /Error ID:\s*[0-9a-fA-F-]*/i.test(text) || /(?:⚠️|⚠\uFE0F?)\s+.*(?:high[- ]traffic|try again in a minute|try again later|stream was interrupted|stream interrupted|overloaded|unexpected error|quota\b|upgrade your subscription|increase your limits|resets in\s+\d|credits balance is too low|out of credits|rate[- ]limit|capacity exhausted|exhausted your quota|internal error|currently unreachable|network issue connecting|authentication expired|authentication issue|terminated due to error|invalid output|invalid tool call)/i.test(text)
3099
3102
  );
3100
3103
  }
3101
3104
  function isNonSystemErrorWarningLine(trimmed) {
@@ -4216,7 +4219,7 @@ function isQuestionFooterLine(line) {
4216
4219
  }
4217
4220
  if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) return false;
4218
4221
  if (isQuestionHeaderLine(trimmed)) return false;
4219
- return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?select)?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?select(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
4222
+ return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:space|<space>)\s+(?:to\s+)?(?:select|toggle)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|Submit(?:\s+All)?|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?$/i.test(trimmed) || /^(?:←|<-)\s*Back\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
4220
4223
  }
4221
4224
  function extractQuestionHeader(snapshot) {
4222
4225
  const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
@@ -4242,7 +4245,6 @@ function hashPromptSnapshot(snapshot) {
4242
4245
  }
4243
4246
  return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
4244
4247
  }
4245
- var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
4246
4248
  function escapeShellArg(value) {
4247
4249
  return value.replace(/'/g, `'\\''`);
4248
4250
  }
@@ -4387,33 +4389,8 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
4387
4389
  const options = parseOptions(belowHeader);
4388
4390
  if (options.length < 2) return null;
4389
4391
  const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
4390
- const headerHasArrowHint = /\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)/i.test(normalizedHeader);
4391
- let footerHasArrowHint = false;
4392
- let footerHasCtrlRReview = false;
4393
- const startLine = headerIdx >= 0 ? headerIdx + 1 : 0;
4394
- let hasSeenOptions = false;
4395
- for (let i = startLine; i < lines.length; i++) {
4396
- if (lineInFence[i]) continue;
4397
- const trimmed = lines[i].trim();
4398
- if (!trimmed) continue;
4399
- if (hasSeenOptions && isResumedTurnSignal(trimmed)) break;
4400
- if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
4401
- hasSeenOptions = true;
4402
- }
4403
- const isFooter = isQuestionFooterLine(trimmed) || /^(?:ctrl\+[a-z0-9]+\s+[a-zA-Z]+|\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed);
4404
- if (isFooter) {
4405
- if (/(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate\b/i.test(trimmed) || /\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?/i.test(trimmed)) {
4406
- footerHasArrowHint = true;
4407
- }
4408
- if (/ctrl\+r\s+Review\b/i.test(trimmed)) {
4409
- footerHasCtrlRReview = true;
4410
- }
4411
- }
4412
- }
4413
- const hasArrowKeyHint = headerHasArrowHint || footerHasArrowHint;
4414
- const hasWriteInOption = options.some((opt) => /^\s*write-?in\b/i.test(opt.text));
4415
- const hasCtrlRReviewFooter = footerHasCtrlRReview;
4416
- const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !hasArrowKeyHint && !hasWriteInOption && !hasCtrlRReviewFooter;
4392
+ const isMultiSelect = /\bselect\s+(?:all|multiple)\b/i.test(normalizedHeader) || /\bselect\s+(?:all|multiple)\b/i.test(belowHeader) || /(?:<space>|\bspace)\s+(?:to\s+)?(?:select|toggle)\b/i.test(belowHeader) || /\bctrl\+a\s+select\s+all\b/i.test(belowHeader);
4393
+ const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !isMultiSelect;
4417
4394
  const submitMap = isBaseNumberedWizard ? buildQuestionSubmitMap(options) : {};
4418
4395
  const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
4419
4396
  return {
@@ -4476,12 +4453,47 @@ function buildApprovalSubmitMap(options) {
4476
4453
  function buildQuestionSubmitMap(options) {
4477
4454
  const map = {};
4478
4455
  for (const opt of options) {
4479
- if (!/^\s*write-?in\b/i.test(opt.text)) {
4480
- map[opt.number] = [opt.number, "Enter"];
4481
- }
4456
+ map[opt.number] = [opt.number];
4482
4457
  }
4483
4458
  return map;
4484
4459
  }
4460
+ function isWriteInInputActive(paneText, expectedHeader, expectedStep) {
4461
+ if (!paneText) return false;
4462
+ if (detectActivePromptKind(paneText) !== "question") return false;
4463
+ const stripped = paneText.replace(ANSI_ESCAPE_REGEX2, "");
4464
+ const lines = stripped.trimEnd().split("\n");
4465
+ const windowStart = Math.max(0, lines.length - 30);
4466
+ let headerIdx = -1;
4467
+ for (let i = lines.length - 1; i >= windowStart; i--) {
4468
+ if (isQuestionHeaderLine(lines[i])) {
4469
+ headerIdx = i;
4470
+ break;
4471
+ }
4472
+ }
4473
+ if (headerIdx < 0) return false;
4474
+ const rawHeader = lines[headerIdx];
4475
+ const extracted = extractQuestionHeader(rawHeader);
4476
+ if (!extracted) return false;
4477
+ if (expectedHeader) {
4478
+ const expectedExtracted = extractQuestionHeader(expectedHeader);
4479
+ const expectedBody = (expectedExtracted ? expectedExtracted.body : expectedHeader).replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
4480
+ const paneBody = extracted.body.replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
4481
+ if (paneBody !== expectedBody) {
4482
+ return false;
4483
+ }
4484
+ }
4485
+ const expectedStepVal = expectedStep || (expectedHeader ? expectedHeader.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1] : void 0);
4486
+ if (expectedStepVal) {
4487
+ const stepMatch = extracted.fullLine.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i);
4488
+ const paneStep = stepMatch ? stepMatch[1] : null;
4489
+ if (!paneStep || paneStep !== expectedStepVal) {
4490
+ return false;
4491
+ }
4492
+ }
4493
+ const searchLines = lines.slice(headerIdx);
4494
+ const text = searchLines.join("\n");
4495
+ return /Your answer:/i.test(text) && /enter\s+Submit(?:\s+All)?\s*[·│,]\s*esc\s+Back/i.test(text);
4496
+ }
4485
4497
  function extractIdentity(headerText) {
4486
4498
  if (!headerText) return {};
4487
4499
  const trimmed = headerText.trim();
@@ -5207,13 +5219,15 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5207
5219
  const qOptTexts = (targetQ.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
5208
5220
  const cOptTexts = (candidate.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
5209
5221
  if (cOptTexts.length > 0 && qOptTexts.length > 0) {
5210
- const matchedQCount = qOptTexts.filter((q) => cOptTexts.includes(q)).length;
5211
- const matchedCCount = cOptTexts.filter((c) => qOptTexts.includes(c)).length;
5222
+ const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
5223
+ const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
5224
+ const matchedQCount = qOptTexts.filter((q) => cEffectiveTexts.includes(q)).length;
5225
+ const matchedCCount = cEffectiveTexts.filter((c) => qOptTexts.includes(c)).length;
5212
5226
  const qRatio = matchedQCount / qOptTexts.length;
5213
- if (cOptTexts.length !== qOptTexts.length || matchedQCount !== qOptTexts.length || matchedCCount !== cOptTexts.length) {
5227
+ if (cEffectiveTexts.length !== qOptTexts.length || matchedQCount !== qOptTexts.length || matchedCCount !== cEffectiveTexts.length) {
5214
5228
  return { score: -1, textMatch: true };
5215
5229
  }
5216
- const posMatches = qOptTexts.filter((q, idx) => idx < cOptTexts.length && cOptTexts[idx] === q).length;
5230
+ const posMatches = qOptTexts.filter((q, idx) => idx < cEffectiveTexts.length && cEffectiveTexts[idx] === q).length;
5217
5231
  const posRatio = posMatches / qOptTexts.length;
5218
5232
  const score = qRatio * 0.5 + posRatio * 0.5;
5219
5233
  return { score, textMatch: true };
@@ -5246,13 +5260,19 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5246
5260
  });
5247
5261
  return null;
5248
5262
  }
5263
+ const targetStepQ = sqs[bestStepIdx];
5264
+ if (targetStepQ?.isMultiSelect) {
5265
+ candidate.submitMap = {};
5266
+ }
5249
5267
  const isAdvancement = bestStepIdx !== currentActiveIdx;
5250
5268
  if (!isAdvancement) {
5251
5269
  const existing = [...this.pendingPrompts.values()].find(
5252
5270
  (p) => p.conversationId === conversationId && p.kind === "question"
5253
5271
  );
5254
5272
  if (existing) {
5255
- const isInitialUpgrade = existing.isHookPrompt && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
5273
+ const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
5274
+ const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
5275
+ const isInitialUpgrade = existing.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
5256
5276
  if (isInitialUpgrade) {
5257
5277
  existing.submitMap = candidate.submitMap;
5258
5278
  if (candidate.options && candidate.options.length > 0) {
@@ -5271,12 +5291,30 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5271
5291
  this.emit("prompt-upgraded", existing);
5272
5292
  return existing;
5273
5293
  }
5274
- if (!existing.paneSemanticKey) {
5275
- existing.paneSemanticKey = candidateSemanticKey;
5294
+ const areControlsMutated = (existingOpts, candidateOpts) => {
5295
+ if (!existingOpts || !candidateOpts) return false;
5296
+ if (existingOpts.length !== candidateOpts.length) return true;
5297
+ return !existingOpts.every((o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim());
5298
+ };
5299
+ const isControlDelta = Boolean(
5300
+ existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
5301
+ );
5302
+ if (isControlDelta) {
5303
+ logger.info("Control delta observed on active question step; resolving old prompt and re-emitting", {
5304
+ oldPromptId: existing.promptId,
5305
+ oldSemanticKey: existing.paneSemanticKey,
5306
+ newSemanticKey: candidateSemanticKey
5307
+ });
5308
+ this.resolvePrompt(existing.promptId, { advanceWizard: false });
5309
+ this.emit("prompt-resolved", existing.promptId);
5310
+ } else {
5311
+ if (!existing.paneSemanticKey) {
5312
+ existing.paneSemanticKey = candidateSemanticKey;
5313
+ }
5314
+ if (candidate.body) existing.body = candidate.body;
5315
+ if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5316
+ return null;
5276
5317
  }
5277
- if (candidate.body) existing.body = candidate.body;
5278
- if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5279
- return null;
5280
5318
  }
5281
5319
  } else {
5282
5320
  liveWizard2.activeStepIndex = bestStepIdx;
@@ -5298,12 +5336,14 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5298
5336
  const hookQuestionText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
5299
5337
  const isSameQuestion = questionsMatch(hookQuestionText, paneQuestionText);
5300
5338
  if (isSameQuestion) {
5301
- const isInitialUpgrade = p.isHookPrompt && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
5339
+ const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
5340
+ const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
5341
+ const isInitialUpgrade = p.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
5302
5342
  const isControlDelta = !isInitialUpgrade && Boolean(
5303
5343
  p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
5304
5344
  );
5305
5345
  if (!isControlDelta) {
5306
- if (p.isHookPrompt && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0) {
5346
+ if (isInitialUpgrade) {
5307
5347
  p.submitMap = candidate.submitMap;
5308
5348
  if (candidate.options && candidate.options.length > 0) {
5309
5349
  p.paneOptions = candidate.options;
@@ -5424,6 +5464,16 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5424
5464
  } else {
5425
5465
  p.pendingCall = call;
5426
5466
  }
5467
+ const isHookMultiSelect = Boolean(
5468
+ call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
5469
+ );
5470
+ if (isHookMultiSelect && Object.keys(p.submitMap || {}).length > 0) {
5471
+ p.submitMap = {};
5472
+ logger.info("emitHookQuestionPrompt: revoked actionable submitMap because incoming hook is multi-select", {
5473
+ promptId: p.promptId
5474
+ });
5475
+ this.emit("prompt-upgraded", p);
5476
+ }
5427
5477
  p.isHookPrompt = true;
5428
5478
  call.matchedPromptId = p.promptId;
5429
5479
  if (call.structuredQuestions && call.structuredQuestions.length > 0) {
@@ -5961,23 +6011,22 @@ var PromptResponder = class {
5961
6011
  }
5962
6012
  /**
5963
6013
  * Resolve a pending approval prompt with the mobile-user's chosen
5964
- * option number. Uses the submitMap from the PendingPromptState to
5965
- * translate option-number → terminal keystrokes (digit alone for agy's
5966
- * 4-numbered UI; future Y/N prompts would use 'y'/'n'/'Escape').
6014
+ * option number and optional follow-up text for write-in questions.
6015
+ * Uses the submitMap from the PendingPromptState to translate
6016
+ * option-number → terminal keystrokes.
5967
6017
  *
5968
6018
  * Active-prompt-identity guard (DESIGN.md §3.4):
5969
6019
  * 1. Detector check: is `promptId` still in pendingPrompts? If not
5970
6020
  * (expired or already-resolved), drop with `prompt-expired`.
5971
- * 2. Pane-observer check: probeApprovalUIActive() — does the pane
5972
- * STILL show an approval UI whose header matches our pending
5973
- * call's identity? If active-UI is gone OR header differs,
5974
- * drop with `prompt-superseded`.
5975
- * 3. submitMap lookup: is `optionNumber` a valid key? If not (e.g.
5976
- * mobile sent "5" for a 4-option prompt), drop with
5977
- * `invalid-option`.
5978
- * 4. Send the keystrokes via tmux send-keys.
6021
+ * 2. submitMap lookup: is `optionNumber` a valid key? If not, drop with `invalid-option`.
6022
+ * If `followUpText` is provided, verify option is Write-in; otherwise drop with `follow-up-not-allowed`.
6023
+ * If bare Write-in option has no text, drop with `write-in-text-required`.
6024
+ * 3. Pane-observer check: captureSnapshot() — does the pane STILL show
6025
+ * an approval UI matching our pending prompt's semantic key?
6026
+ * 4. Send option keystrokes via tmux send-keys.
6027
+ * 5. If write-in followUpText is present: wait 250ms, verify pane state, type literal text, send Enter.
5979
6028
  */
5980
- async sendApprovalReply(promptId, optionNumber) {
6029
+ async sendApprovalReply(promptId, optionNumber, followUpText, beforeTypeText) {
5981
6030
  const target = this.resolveTmuxTarget();
5982
6031
  if (!target) {
5983
6032
  return { ok: false, reason: "no-tmux-target" };
@@ -5992,15 +6041,6 @@ var PromptResponder = class {
5992
6041
  details: `promptId ${promptId} is not in pendingPrompts`
5993
6042
  };
5994
6043
  }
5995
- if (this.paneObserver) {
5996
- if (!state.paneSemanticKey) {
5997
- return {
5998
- ok: false,
5999
- reason: "prompt-probe-failed",
6000
- details: "pending prompt has no exact semantic identity"
6001
- };
6002
- }
6003
- }
6004
6044
  const keys = state.submitMap[optionNumber];
6005
6045
  if (!keys || keys.length === 0) {
6006
6046
  return {
@@ -6009,21 +6049,86 @@ var PromptResponder = class {
6009
6049
  details: `option '${optionNumber}' not in submitMap (valid: ${Object.keys(state.submitMap).join(",")})`
6010
6050
  };
6011
6051
  }
6052
+ const isQuestionPrompt = state.kind === "question";
6053
+ const expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
6054
+ const matchingOpt = state.paneOptions?.find((o) => String(o.number) === String(optionNumber));
6055
+ const isWriteInOption = matchingOpt ? /^\s*write-?in\b/i.test(matchingOpt.text) : false;
6056
+ if (isQuestionPrompt) {
6057
+ if (state.paneOptions && state.paneOptions.length > 0 && !matchingOpt) {
6058
+ return {
6059
+ ok: false,
6060
+ reason: "invalid-option",
6061
+ details: `option '${optionNumber}' not found in advertised options`,
6062
+ deliveryPhase: "none"
6063
+ };
6064
+ }
6065
+ if (isWriteInOption && (!followUpText || followUpText.trim().length === 0)) {
6066
+ return {
6067
+ ok: false,
6068
+ reason: "write-in-text-required",
6069
+ details: 'Please provide your write-in text along with the option number (e.g. "4, your response")',
6070
+ deliveryPhase: "none"
6071
+ };
6072
+ }
6073
+ if (followUpText && followUpText.trim().length > 0 && !isWriteInOption) {
6074
+ return {
6075
+ ok: false,
6076
+ reason: "follow-up-not-allowed",
6077
+ details: `Option '${optionNumber}' does not accept follow-up text`,
6078
+ deliveryPhase: "none"
6079
+ };
6080
+ }
6081
+ }
6082
+ if (this.paneObserver) {
6083
+ if (!state.paneSemanticKey) {
6084
+ return {
6085
+ ok: false,
6086
+ reason: "prompt-probe-failed",
6087
+ details: "pending prompt has no exact semantic identity",
6088
+ deliveryPhase: "none"
6089
+ };
6090
+ }
6091
+ }
6092
+ let deliveryPhase = "none";
6012
6093
  try {
6013
6094
  for (const key of keys) {
6014
6095
  if (!this.detector || !this.paneObserver) {
6015
6096
  return {
6016
6097
  ok: false,
6017
6098
  reason: "prompt-probe-failed",
6018
- details: "approval responder is missing its detector or pane observer"
6099
+ details: "approval responder is missing its detector or pane observer",
6100
+ deliveryPhase
6019
6101
  };
6020
6102
  }
6021
- if (this.detector.getPendingPrompt(promptId) !== state) {
6022
- return { ok: false, reason: "prompt-expired" };
6103
+ const currentPrompt = this.detector.getPendingPrompt(promptId);
6104
+ if (currentPrompt !== state || !currentPrompt.submitMap || !currentPrompt.submitMap[optionNumber]) {
6105
+ return {
6106
+ ok: false,
6107
+ reason: "prompt-expired",
6108
+ details: "option is no longer actionable on active prompt (controls revoked or updated)",
6109
+ deliveryPhase
6110
+ };
6023
6111
  }
6024
6112
  let liveSemanticKey = null;
6025
6113
  try {
6026
6114
  const snapshot = await this.paneObserver.captureSnapshot();
6115
+ const postCapturePrompt = this.detector.getPendingPrompt(promptId);
6116
+ if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
6117
+ return {
6118
+ ok: false,
6119
+ reason: "prompt-expired",
6120
+ details: "option is no longer actionable on active prompt (controls revoked or updated)",
6121
+ deliveryPhase
6122
+ };
6123
+ }
6124
+ if (isQuestionPrompt && isWriteInInputActive(snapshot, state.matchedPaneHeader, expectedStep)) {
6125
+ return {
6126
+ ok: false,
6127
+ reason: "write-in-already-active",
6128
+ details: "write-in input field is already open on desktop; finish or cancel on desktop before selecting an option",
6129
+ deliveryPhase: "none"
6130
+ };
6131
+ }
6027
6132
  const live = parseApprovalSnapshot(snapshot);
6028
6133
  if (!live) {
6029
6134
  if (detectActivePromptKind(snapshot) === null) {
@@ -6031,13 +6136,15 @@ var PromptResponder = class {
6031
6136
  return {
6032
6137
  ok: false,
6033
6138
  reason: "prompt-superseded",
6034
- details: "the approval chooser is no longer active"
6139
+ details: "the approval chooser is no longer active",
6140
+ deliveryPhase
6035
6141
  };
6036
6142
  }
6037
6143
  return {
6038
6144
  ok: false,
6039
6145
  reason: "prompt-probe-failed",
6040
- details: "the active chooser could not be parsed exactly"
6146
+ details: "the active chooser could not be parsed exactly",
6147
+ deliveryPhase
6041
6148
  };
6042
6149
  }
6043
6150
  liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
@@ -6045,7 +6152,8 @@ var PromptResponder = class {
6045
6152
  return {
6046
6153
  ok: false,
6047
6154
  reason: "prompt-probe-failed",
6048
- details: error instanceof Error ? error.message : String(error)
6155
+ details: error instanceof Error ? error.message : String(error),
6156
+ deliveryPhase
6049
6157
  };
6050
6158
  }
6051
6159
  if (liveSemanticKey !== state.paneSemanticKey) {
@@ -6053,32 +6161,147 @@ var PromptResponder = class {
6053
6161
  return {
6054
6162
  ok: false,
6055
6163
  reason: "prompt-superseded",
6056
- details: "active chooser semantics differ from the mobile prompt"
6164
+ details: "active chooser semantics differ from the mobile prompt",
6165
+ deliveryPhase
6057
6166
  };
6058
6167
  }
6168
+ deliveryPhase = "option_attempted";
6059
6169
  if (isNamedKey(key)) {
6060
6170
  await this.sendKey(target, key);
6061
6171
  } else {
6062
6172
  await this.typeLiteral(target, key);
6063
6173
  }
6174
+ deliveryPhase = "option_sent";
6175
+ await delay(50);
6176
+ }
6177
+ if (isQuestionPrompt && isWriteInOption && followUpText && followUpText.trim().length > 0) {
6178
+ let writeInReady = false;
6179
+ if (this.paneObserver) {
6180
+ for (let probeAttempt = 0; probeAttempt < 10; probeAttempt++) {
6181
+ await delay(100);
6182
+ try {
6183
+ const liveSnap = await this.paneObserver.captureSnapshot();
6184
+ if (liveSnap && isWriteInInputActive(liveSnap, state.matchedPaneHeader, expectedStep)) {
6185
+ writeInReady = true;
6186
+ break;
6187
+ }
6188
+ } catch {
6189
+ }
6190
+ }
6191
+ if (!writeInReady) {
6192
+ return {
6193
+ ok: false,
6194
+ reason: "prompt-probe-failed",
6195
+ details: "terminal pane failed to transition to write-in input line",
6196
+ deliveryPhase
6197
+ };
6198
+ }
6199
+ } else {
6200
+ await delay(250);
6201
+ }
6202
+ if (beforeTypeText) {
6203
+ try {
6204
+ await beforeTypeText();
6205
+ } catch (err) {
6206
+ return {
6207
+ ok: false,
6208
+ reason: "tmux-failed",
6209
+ details: err instanceof Error ? err.message : String(err),
6210
+ deliveryPhase: "option_sent"
6211
+ // Do not advance phase if hook throws (Claude R4-N2)
6212
+ };
6213
+ }
6214
+ }
6215
+ const postHookPrompt = this.detector.getPendingPrompt(promptId);
6216
+ if (postHookPrompt !== state || !postHookPrompt.submitMap || !postHookPrompt.submitMap[optionNumber]) {
6217
+ return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
6218
+ }
6219
+ if (this.paneObserver) {
6220
+ try {
6221
+ const postHookSnap = await this.paneObserver.captureSnapshot();
6222
+ if (!postHookSnap || !isWriteInInputActive(postHookSnap, state.matchedPaneHeader, expectedStep)) {
6223
+ return {
6224
+ ok: false,
6225
+ reason: "prompt-probe-failed",
6226
+ details: "write-in input field disappeared during callback before typing",
6227
+ deliveryPhase: "option_sent"
6228
+ };
6229
+ }
6230
+ } catch (err) {
6231
+ return {
6232
+ ok: false,
6233
+ reason: "prompt-probe-failed",
6234
+ details: err instanceof Error ? err.message : String(err),
6235
+ deliveryPhase: "option_sent"
6236
+ };
6237
+ }
6238
+ const postSnapPrompt1 = this.detector.getPendingPrompt(promptId);
6239
+ if (postSnapPrompt1 !== state || !postSnapPrompt1.submitMap || !postSnapPrompt1.submitMap[optionNumber]) {
6240
+ return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
6241
+ }
6242
+ }
6243
+ deliveryPhase = "text_attempted";
6244
+ const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
6245
+ await this.typeLiteral(target, cleanText);
6246
+ deliveryPhase = "text_typed";
6247
+ await delay(50);
6248
+ if (this.paneObserver) {
6249
+ try {
6250
+ const preEnterSnap = await this.paneObserver.captureSnapshot();
6251
+ if (!preEnterSnap || !isWriteInInputActive(preEnterSnap, state.matchedPaneHeader, expectedStep)) {
6252
+ return {
6253
+ ok: false,
6254
+ reason: "prompt-probe-failed",
6255
+ details: "write-in input field disappeared before confirmation",
6256
+ deliveryPhase: "text_typed"
6257
+ };
6258
+ }
6259
+ } catch (err) {
6260
+ return {
6261
+ ok: false,
6262
+ reason: "prompt-probe-failed",
6263
+ details: err instanceof Error ? err.message : String(err),
6264
+ deliveryPhase: "text_typed"
6265
+ };
6266
+ }
6267
+ }
6268
+ const postSnapPrompt = this.detector.getPendingPrompt(promptId);
6269
+ if (postSnapPrompt !== state || !postSnapPrompt.submitMap || !postSnapPrompt.submitMap[optionNumber]) {
6270
+ return { ok: false, reason: "prompt-expired", deliveryPhase: "text_typed" };
6271
+ }
6272
+ deliveryPhase = "enter_attempted";
6273
+ await this.sendKey(target, "Enter");
6274
+ deliveryPhase = "completed";
6064
6275
  }
6065
6276
  this.detector.resolvePrompt(promptId);
6066
6277
  logger.info("Sent approval reply via tmux", {
6067
6278
  promptId,
6068
6279
  optionNumber,
6069
- keysSent: keys
6280
+ keysSent: keys,
6281
+ hasFollowUpText: Boolean(followUpText),
6282
+ deliveryPhase
6070
6283
  });
6071
- return { ok: true };
6284
+ return { ok: true, deliveryPhase: "completed" };
6072
6285
  } catch (error) {
6073
6286
  return {
6074
6287
  ok: false,
6075
6288
  reason: "tmux-failed",
6076
- details: error instanceof Error ? error.message : String(error)
6289
+ details: error instanceof Error ? error.message : String(error),
6290
+ deliveryPhase
6077
6291
  };
6078
6292
  }
6079
6293
  }
6080
6294
  try {
6081
6295
  await this.typeLiteral(target, optionNumber);
6296
+ await delay(50);
6297
+ await this.sendKey(target, "Enter");
6298
+ if (followUpText && followUpText.trim().length > 0) {
6299
+ await delay(250);
6300
+ const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
6301
+ await this.typeLiteral(target, cleanText);
6302
+ await delay(50);
6303
+ await this.sendKey(target, "Enter");
6304
+ }
6082
6305
  return { ok: true };
6083
6306
  } catch (error) {
6084
6307
  return {
@@ -9086,9 +9309,7 @@ var McpServer = class _McpServer {
9086
9309
  let e1rec;
9087
9310
  if (e1existing) {
9088
9311
  e1rec = e1existing.record;
9089
- if (!isDisplayOnly && !e1rec.mobileActionable) {
9090
- e1rec.mobileActionable = true;
9091
- }
9312
+ e1rec.mobileActionable = !isDisplayOnly;
9092
9313
  } else {
9093
9314
  e1rec = this.newE1Raise(session.sessionId, !isDisplayOnly);
9094
9315
  const oldPromptId = state.promptId;
@@ -9372,22 +9593,119 @@ var McpServer = class _McpServer {
9372
9593
  }
9373
9594
  }
9374
9595
  await this.trackDurableMobileEcho(session, evt, intent.option, "primary");
9375
- const optionResult = await this.promptResponder.sendApprovalReply(
9376
- activePrompt.promptId,
9377
- intent.option
9378
- );
9379
- if (!optionResult.ok) {
9380
- if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) this.mobileDeduper.forget(session.sessionId, intent.option);
9596
+ const isQuestionPrompt = activePrompt.kind === "question";
9597
+ if (isQuestionPrompt) {
9598
+ let armedWriteInEcho = false;
9599
+ const optionResult = intent.followUpText ? await this.promptResponder.sendApprovalReply(
9600
+ activePrompt.promptId,
9601
+ intent.option,
9602
+ intent.followUpText,
9603
+ async () => {
9604
+ armedWriteInEcho = true;
9605
+ await this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form");
9606
+ }
9607
+ ) : await this.promptResponder.sendApprovalReply(
9608
+ activePrompt.promptId,
9609
+ intent.option
9610
+ );
9611
+ if (!optionResult.ok) {
9612
+ if (optionResult.reason === "write-in-text-required") {
9613
+ await this.emitPromptSafetyNotification(
9614
+ session,
9615
+ optionResult.details || 'Please provide your write-in text along with the option number (e.g. "4, your response").'
9616
+ );
9617
+ } else if (optionResult.reason === "write-in-already-active") {
9618
+ await this.emitPromptSafetyNotification(
9619
+ session,
9620
+ optionResult.details || "Write-in input field is already open on desktop; finish or cancel on desktop before selecting an option."
9621
+ );
9622
+ } else if (optionResult.reason === "follow-up-not-allowed") {
9623
+ await this.emitPromptSafetyNotification(
9624
+ session,
9625
+ optionResult.details || `Option ${intent.option} does not accept follow-up text.`
9626
+ );
9627
+ } else {
9628
+ const phase = optionResult.deliveryPhase ?? "none";
9629
+ let message;
9630
+ if (phase !== "none") {
9631
+ message = "Your answer was partially entered on desktop. Please complete or cancel the prompt directly on desktop.";
9632
+ } else if (optionResult.reason === "prompt-expired" || optionResult.reason === "prompt-superseded") {
9633
+ message = "The question prompt on desktop has expired or changed. Please review on desktop.";
9634
+ } else if (optionResult.reason === "prompt-probe-failed") {
9635
+ message = "Write-in input field was not ready on desktop. Please try again or respond on desktop.";
9636
+ } else if (optionResult.reason === "no-tmux-target" || optionResult.reason === "tmux-failed") {
9637
+ message = optionResult.details || "Desktop terminal session is unreachable. Please respond directly on desktop.";
9638
+ } else if (optionResult.reason === "display-only-question-active") {
9639
+ message = "This question requires desktop input. Please respond directly on desktop.";
9640
+ } else if (optionResult.reason === "invalid-option") {
9641
+ message = optionResult.details || `Option '${intent.option}' is not valid for the current question.`;
9642
+ } else {
9643
+ message = optionResult.details || `Failed to submit response to question prompt (${optionResult.reason}).`;
9644
+ }
9645
+ await this.emitPromptSafetyNotification(session, message);
9646
+ }
9647
+ if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
9648
+ const phase = optionResult.deliveryPhase ?? "none";
9649
+ if (phase === "none") {
9650
+ this.mobileDeduper.forget(session.sessionId, intent.option);
9651
+ if (armedWriteInEcho && intent.followUpText) {
9652
+ this.mobileDeduper.forget(session.sessionId, intent.followUpText);
9653
+ }
9654
+ } else if (phase === "option_sent" && armedWriteInEcho && intent.followUpText) {
9655
+ this.mobileDeduper.forget(session.sessionId, intent.followUpText);
9656
+ }
9657
+ }
9658
+ return;
9659
+ }
9660
+ await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
9661
+ await this.markExecuted(evt);
9381
9662
  return;
9382
- }
9383
- await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
9384
- if (!intent.followUpText) {
9663
+ } else {
9664
+ const optionResult = await this.promptResponder.sendApprovalReply(
9665
+ activePrompt.promptId,
9666
+ intent.option
9667
+ );
9668
+ if (!optionResult.ok) {
9669
+ if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
9670
+ const phase = optionResult.deliveryPhase ?? "none";
9671
+ if (phase === "none") {
9672
+ this.mobileDeduper.forget(session.sessionId, intent.option);
9673
+ }
9674
+ }
9675
+ return;
9676
+ }
9677
+ await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
9678
+ if (intent.followUpText) {
9679
+ await new Promise((r) => setTimeout(r, 250));
9680
+ const freeFormResult = await this.promptResponder.sendFreeFormWhenNoApprovalActive(
9681
+ intent.followUpText,
9682
+ () => this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form")
9683
+ );
9684
+ if (!freeFormResult.ok) {
9685
+ if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
9686
+ this.mobileDeduper.forget(session.sessionId, intent.followUpText);
9687
+ }
9688
+ logger.warn("Follow-up text send failed for approval", { reason: freeFormResult.reason });
9689
+ return;
9690
+ }
9691
+ }
9385
9692
  await this.markExecuted(evt);
9386
9693
  return;
9387
9694
  }
9388
- promptContent = intent.followUpText;
9389
- await new Promise((resolve2) => setTimeout(resolve2, 250));
9390
9695
  } else {
9696
+ const activeQuestionPrompt = pendingPrompts.find((p) => p.kind === "question");
9697
+ if (activeQuestionPrompt) {
9698
+ const isDisplayOnly = Object.keys(activeQuestionPrompt.submitMap || {}).length === 0;
9699
+ logger.warn("Non-option input received while an interactive question prompt is active; rejecting dismissal", {
9700
+ sessionId: session.sessionId,
9701
+ promptId: activeQuestionPrompt.promptId,
9702
+ isDisplayOnly,
9703
+ content: promptContent.slice(0, 64)
9704
+ });
9705
+ const message = isDisplayOnly ? "The active terminal question prompt is display-only on mobile. Please select an option directly in your terminal." : "An interactive question prompt is active in your terminal. Please select one of the advertised options or answer directly in your terminal.";
9706
+ await this.emitPromptSafetyNotification(session, message);
9707
+ return;
9708
+ }
9391
9709
  const hasDisplayOnlyPrompt = pendingPrompts.some(
9392
9710
  (p) => Object.keys(p.submitMap || {}).length === 0
9393
9711
  );
@@ -10057,6 +10375,7 @@ var __testing = {
10057
10375
  0 && (module.exports = {
10058
10376
  ApprovalDetector,
10059
10377
  McpServer,
10378
+ PromptResponder,
10060
10379
  SessionNotFoundError,
10061
10380
  StoredAuthFailureError,
10062
10381
  TERMINAL_SHUTDOWN_SIGNALS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.85",
3
+ "version": "2.0.86",
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": {