@quantiya/codevibe-antigravity-plugin 2.0.79 → 2.0.81

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 +802 -64
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -34,16 +34,19 @@ __export(server_exports, {
34
34
  SessionNotFoundError: () => SessionNotFoundError,
35
35
  StoredAuthFailureError: () => StoredAuthFailureError,
36
36
  TERMINAL_SHUTDOWN_SIGNALS: () => TERMINAL_SHUTDOWN_SIGNALS,
37
+ TmuxPaneObserver: () => TmuxPaneObserver,
37
38
  __testing: () => __testing,
38
39
  classifyEntrypoint: () => classifyEntrypoint,
39
40
  classifyTmuxHasSessionError: () => classifyTmuxHasSessionError,
40
41
  commandDigest: () => commandDigest,
41
42
  detectActivePromptKind: () => detectActivePromptKind,
43
+ extractFileAccessPrompt: () => extractFileAccessPrompt,
42
44
  extractSystemErrorPrompt: () => extractSystemErrorPrompt,
43
45
  generateLaunchSessionId: () => generateLaunchSessionId,
44
46
  getActiveConversationFromCliLog: () => getActiveConversationFromCliLog,
45
47
  isAgyStatusFooterLine: () => isAgyStatusFooterLine,
46
48
  isComposedStatusFooter: () => isComposedStatusFooter,
49
+ isPaneInputSafetyBlocked: () => isPaneInputSafetyBlocked,
47
50
  isSystemErrorActiveFromIndex: () => isSystemErrorActiveFromIndex,
48
51
  looksLikePromptSnapshot: () => looksLikePromptSnapshot,
49
52
  parseApprovalSnapshot: () => parseApprovalSnapshot,
@@ -2343,7 +2346,9 @@ function promptMatchesCompletedTool(state, toolName, toolArgs) {
2343
2346
  const promptPaths = /* @__PURE__ */ new Set();
2344
2347
  const declared = normalizePath(state.pendingCall?.filePath);
2345
2348
  if (declared) promptPaths.add(declared);
2346
- for (const link of fileLinks(state.paneDisplayHeader)) promptPaths.add(link);
2349
+ if (!isFileAccessPrompt(state) || !declared) {
2350
+ for (const link of fileLinks(state.paneDisplayHeader)) promptPaths.add(link);
2351
+ }
2347
2352
  if (promptPaths.size === 0) return false;
2348
2353
  for (const key of TOOL_PATH_KEYS) {
2349
2354
  const toolPath = normalizePath(toolArgs[key]);
@@ -2603,8 +2608,24 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2603
2608
  headerText = extractHeader(snapshot);
2604
2609
  }
2605
2610
  }
2606
- const isChooser = hasPrompt && kind !== "system_error";
2607
- return { active: isChooser, headerText, probeSucceeded: true, kind };
2611
+ let isChooser = hasPrompt && kind !== "system_error";
2612
+ let reportedKind = kind;
2613
+ let isAmbiguous = false;
2614
+ if (!isChooser && kind !== "system_error") {
2615
+ const safety = isPaneInputSafetyBlocked(snapshot);
2616
+ if (safety.blocked) {
2617
+ isChooser = true;
2618
+ reportedKind = safety.kind;
2619
+ isAmbiguous = true;
2620
+ }
2621
+ }
2622
+ return {
2623
+ active: isChooser,
2624
+ headerText,
2625
+ probeSucceeded: true,
2626
+ kind: reportedKind,
2627
+ ...isAmbiguous ? { ambiguous: true } : {}
2628
+ };
2608
2629
  } catch {
2609
2630
  return { active: false, headerText: null, probeSucceeded: false, kind: null };
2610
2631
  }
@@ -2864,10 +2885,203 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2864
2885
  }
2865
2886
  }
2866
2887
  };
2888
+ function isComposerBoxAt(lines, idx) {
2889
+ if (idx < 0 || idx >= lines.length) return false;
2890
+ const line0 = lines[idx].trim();
2891
+ if (!/^[─━│╭╰╮╯\s_-]{3,}$/.test(line0)) return false;
2892
+ let cursorIdx = -1;
2893
+ for (let k = idx + 1; k < Math.min(lines.length, idx + 4); k++) {
2894
+ const t = lines[k].trim();
2895
+ if (!t) continue;
2896
+ if (/^[│\s]*[❯>]\s*$/.test(t)) {
2897
+ cursorIdx = k;
2898
+ break;
2899
+ }
2900
+ break;
2901
+ }
2902
+ if (cursorIdx < 0) return false;
2903
+ for (let k = cursorIdx + 1; k < Math.min(lines.length, cursorIdx + 4); k++) {
2904
+ const t = lines[k].trim();
2905
+ if (!t) continue;
2906
+ if (/^[─━│╭╰╮╯\s_-]{3,}$/.test(t)) {
2907
+ return true;
2908
+ }
2909
+ break;
2910
+ }
2911
+ return false;
2912
+ }
2913
+ function isPaneInputSafetyBlocked(input) {
2914
+ const rawText = Array.isArray(input) ? input.join("\n") : input;
2915
+ if (!rawText) return { blocked: false, kind: null };
2916
+ const plainText = rawText.replace(ANSI_ESCAPE_REGEX, "").trimEnd();
2917
+ if (!plainText) return { blocked: false, kind: null };
2918
+ const lines = plainText.split(/\r?\n/);
2919
+ if (lines.length === 0) return { blocked: false, kind: null };
2920
+ const startIdx = Math.max(0, lines.length - 40);
2921
+ let footerIdx = -1;
2922
+ for (let i = lines.length - 1; i >= startIdx; i--) {
2923
+ const trimmed = lines[i].trim();
2924
+ if (!trimmed) continue;
2925
+ if (/^(`{3,}|~{3,})\s*$/.test(lines[i])) {
2926
+ break;
2927
+ }
2928
+ if (isQuestionFooterLine(trimmed)) {
2929
+ footerIdx = i;
2930
+ break;
2931
+ } else if (/\besc to cancel\b/i.test(trimmed) && !isAgyStatusFooterLine(trimmed)) {
2932
+ footerIdx = i;
2933
+ break;
2934
+ }
2935
+ }
2936
+ if (footerIdx < 0) {
2937
+ const candidateHeaderIndices = [];
2938
+ for (let i = lines.length - 1; i >= startIdx; i--) {
2939
+ const trimmed = lines[i].trim();
2940
+ if (!trimmed) continue;
2941
+ if (isQuestionHeaderLine(trimmed) && /\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)/i.test(trimmed)) {
2942
+ candidateHeaderIndices.push(i);
2943
+ }
2944
+ }
2945
+ for (const headerIdx of candidateHeaderIndices) {
2946
+ let optCount = 0;
2947
+ let cursorCount = 0;
2948
+ let lastOptionIdx = -1;
2949
+ let inInnerFence = false;
2950
+ let innerFenceChar = "";
2951
+ let innerFenceLen = 0;
2952
+ let composerEncountered = false;
2953
+ for (let j = headerIdx + 1; j < lines.length; j++) {
2954
+ const lineJ = lines[j];
2955
+ const trimmed = lineJ.trim();
2956
+ if (!trimmed) continue;
2957
+ const fenceMatch = lineJ.match(/^(`{3,}|~{3,})(.*)$/);
2958
+ if (fenceMatch) {
2959
+ const matchedFence = fenceMatch[1];
2960
+ const matchedChar = matchedFence[0];
2961
+ const matchedLen = matchedFence.length;
2962
+ const fenceInfo = fenceMatch[2].trim();
2963
+ if (inInnerFence) {
2964
+ if (matchedChar === innerFenceChar && matchedLen >= innerFenceLen && fenceInfo === "") {
2965
+ inInnerFence = false;
2966
+ innerFenceChar = "";
2967
+ innerFenceLen = 0;
2968
+ optCount = 0;
2969
+ cursorCount = 0;
2970
+ lastOptionIdx = -1;
2971
+ }
2972
+ continue;
2973
+ }
2974
+ inInnerFence = true;
2975
+ innerFenceChar = matchedChar;
2976
+ innerFenceLen = matchedLen;
2977
+ optCount = 0;
2978
+ cursorCount = 0;
2979
+ lastOptionIdx = -1;
2980
+ continue;
2981
+ }
2982
+ if (inInnerFence) {
2983
+ continue;
2984
+ }
2985
+ if (isAgyStatusFooterLine(trimmed) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(trimmed) || isComposerBoxAt(lines, j)) {
2986
+ composerEncountered = true;
2987
+ break;
2988
+ }
2989
+ if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
2990
+ if (/^[>\s❯]*1[.)]/.test(trimmed) && optCount > 0) {
2991
+ optCount = 0;
2992
+ cursorCount = 0;
2993
+ }
2994
+ optCount++;
2995
+ if (/^[>❯]/.test(trimmed)) {
2996
+ cursorCount++;
2997
+ }
2998
+ lastOptionIdx = j;
2999
+ continue;
3000
+ }
3001
+ }
3002
+ const isValidOptionGroup = optCount >= 2 && cursorCount <= 1;
3003
+ if (!inInnerFence && !composerEncountered && isValidOptionGroup && lastOptionIdx >= 0 && !hasTrailingAgyComposer(lines, lastOptionIdx)) {
3004
+ return { blocked: true, kind: "question" };
3005
+ }
3006
+ }
3007
+ return { blocked: false, kind: null };
3008
+ }
3009
+ const candidateHeaders = [];
3010
+ for (let i = footerIdx - 1; i >= startIdx; i--) {
3011
+ const trimmed = lines[i].trim();
3012
+ if (!trimmed) continue;
3013
+ if (isQuestionHeaderLine(trimmed)) {
3014
+ candidateHeaders.push({ lineIdx: i, kind: "question" });
3015
+ } else if (/Allow access to this file\?/i.test(trimmed)) {
3016
+ candidateHeaders.push({ lineIdx: i, kind: "file_access" });
3017
+ } else if (/Requesting permission for:/i.test(trimmed) || /Do you want to (?:proceed|run|execute)/i.test(trimmed)) {
3018
+ candidateHeaders.push({ lineIdx: i, kind: "approval" });
3019
+ }
3020
+ }
3021
+ for (const candidate of candidateHeaders) {
3022
+ let optCount = 0;
3023
+ let cursorCount = 0;
3024
+ let inInnerFence = false;
3025
+ let innerFenceChar = "";
3026
+ let innerFenceLen = 0;
3027
+ let composerEncountered = false;
3028
+ for (let j = candidate.lineIdx + 1; j < footerIdx; j++) {
3029
+ const lineJ = lines[j];
3030
+ const trimmed = lineJ.trim();
3031
+ if (!trimmed) continue;
3032
+ const fenceMatch = lineJ.match(/^(`{3,}|~{3,})(.*)$/);
3033
+ if (fenceMatch) {
3034
+ const marker = fenceMatch[1];
3035
+ const rest = fenceMatch[2].trim();
3036
+ const fChar = marker[0];
3037
+ const fLen = marker.length;
3038
+ if (inInnerFence) {
3039
+ if (fChar === innerFenceChar && fLen >= innerFenceLen && rest === "") {
3040
+ inInnerFence = false;
3041
+ innerFenceChar = "";
3042
+ innerFenceLen = 0;
3043
+ optCount = 0;
3044
+ cursorCount = 0;
3045
+ }
3046
+ continue;
3047
+ }
3048
+ inInnerFence = true;
3049
+ innerFenceChar = fChar;
3050
+ innerFenceLen = fLen;
3051
+ optCount = 0;
3052
+ cursorCount = 0;
3053
+ continue;
3054
+ }
3055
+ if (inInnerFence) {
3056
+ continue;
3057
+ }
3058
+ if (isAgyStatusFooterLine(trimmed) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(trimmed) || isComposerBoxAt(lines, j)) {
3059
+ composerEncountered = true;
3060
+ break;
3061
+ }
3062
+ if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
3063
+ if (/^[>\s❯]*1[.)]/.test(trimmed) && optCount > 0) {
3064
+ optCount = 0;
3065
+ cursorCount = 0;
3066
+ }
3067
+ optCount++;
3068
+ if (/^[>❯]/.test(trimmed)) {
3069
+ cursorCount++;
3070
+ }
3071
+ continue;
3072
+ }
3073
+ }
3074
+ const isValidOptionGroup = optCount >= 2 && cursorCount <= 1;
3075
+ if (!inInnerFence && !composerEncountered && isValidOptionGroup && !hasTrailingAgyComposer(lines, footerIdx)) {
3076
+ return { blocked: true, kind: candidate.kind };
3077
+ }
3078
+ }
3079
+ return { blocked: false, kind: null };
3080
+ }
2867
3081
  function looksLikePromptDelta(chunk) {
2868
3082
  return (
2869
3083
  // Approval UI signatures
2870
- /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) || /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)
3084
+ /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)
2871
3085
  );
2872
3086
  }
2873
3087
  function isNonSystemErrorWarningLine(trimmed) {
@@ -2936,7 +3150,13 @@ function isComposedStatusFooter(trimmed) {
2936
3150
  }
2937
3151
  function isAgyStatusFooterLine(trimmed) {
2938
3152
  if (!trimmed) return false;
2939
- if (/^\s*(?:\?\s+for shortcuts|esc to cancel)\b/i.test(trimmed)) {
3153
+ if (/(?:\benter|<enter>)\s+(?:to\s+)?select\b/i.test(trimmed) || /\bNavigate\b/i.test(trimmed)) {
3154
+ return false;
3155
+ }
3156
+ if (/^\s*(?:[❯>]\s*)?\?\s+for shortcuts\b/i.test(trimmed)) {
3157
+ return true;
3158
+ }
3159
+ if (/^\s*esc to cancel\b/i.test(trimmed)) {
2940
3160
  return true;
2941
3161
  }
2942
3162
  if (/^(?:\(initializing\.\.\.\)|\d+\s+task\(s\)\s+·\s+\/tasks|\d+\s+subagent\s*\(s\)|AI:\s+(?:\d+\s+\(\/credits to add more\)|Out of credits)|AI Credits:\s+\d+|Quota:\s*\d+(?:\.\d+)?%)$/i.test(trimmed)) {
@@ -2947,6 +3167,68 @@ function isAgyStatusFooterLine(trimmed) {
2947
3167
  }
2948
3168
  return false;
2949
3169
  }
3170
+ function computeFenceMask(lines) {
3171
+ const inFence = new Array(lines.length).fill(false);
3172
+ let inside = false;
3173
+ let activeFenceChar = "";
3174
+ let activeFenceLen = 0;
3175
+ for (let i = 0; i < lines.length; i++) {
3176
+ const line = lines[i];
3177
+ const match = line.match(/^(`{3,}|~{3,})(.*)$/);
3178
+ if (match) {
3179
+ const marker = match[1];
3180
+ const rest = match[2];
3181
+ const fenceChar = marker[0];
3182
+ const fenceLen = marker.length;
3183
+ if (!inside) {
3184
+ inside = true;
3185
+ activeFenceChar = fenceChar;
3186
+ activeFenceLen = fenceLen;
3187
+ inFence[i] = true;
3188
+ continue;
3189
+ } else {
3190
+ if (fenceChar === activeFenceChar && fenceLen >= activeFenceLen && rest.trim() === "") {
3191
+ inside = false;
3192
+ activeFenceChar = "";
3193
+ activeFenceLen = 0;
3194
+ inFence[i] = true;
3195
+ continue;
3196
+ }
3197
+ }
3198
+ }
3199
+ inFence[i] = inside;
3200
+ }
3201
+ return inFence;
3202
+ }
3203
+ function hasTrailingAgyComposer(lines, afterLineIdx, fenceMask) {
3204
+ if (afterLineIdx < -1 || afterLineIdx >= lines.length - 1) return false;
3205
+ const trailingLines = [];
3206
+ for (let i = afterLineIdx + 1; i < lines.length; i++) {
3207
+ if (fenceMask && fenceMask[i]) continue;
3208
+ const trimmed = lines[i].trim();
3209
+ if (trimmed.length > 0) {
3210
+ trailingLines.push(trimmed);
3211
+ }
3212
+ }
3213
+ if (trailingLines.length === 0) return false;
3214
+ if (trailingLines.some((l) => /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(l))) {
3215
+ return true;
3216
+ }
3217
+ for (let i = 0; i < trailingLines.length; i++) {
3218
+ const l = trailingLines[i];
3219
+ if (/^[─━│╭╰╮╯\s_-]{3,}$/.test(l)) {
3220
+ if (i + 1 < trailingLines.length && /^[│\s]*[❯>]\s*$/.test(trailingLines[i + 1])) {
3221
+ if (i + 2 < trailingLines.length && /^[─━│╭╰╮╯\s_-]{3,}$/.test(trailingLines[i + 2])) {
3222
+ return true;
3223
+ }
3224
+ }
3225
+ }
3226
+ }
3227
+ if (trailingLines.some((l) => isAgyStatusFooterLine(l) && /(?:Gemini|Flash|Pro|Claude|Sonnet|Opus|GPT|auto|high|low|medium)/i.test(l))) {
3228
+ return true;
3229
+ }
3230
+ return false;
3231
+ }
2950
3232
  function reconstructFooter(rawLines) {
2951
3233
  const cleaned = rawLines.map((l) => l.trim()).filter(Boolean);
2952
3234
  if (cleaned.length === 0) return "";
@@ -3098,17 +3380,62 @@ function looksLikePromptSnapshot(snapshot) {
3098
3380
  return hasApprovalHeader && hasApprovalOptions && hasApprovalFooter;
3099
3381
  }
3100
3382
  if (activeKind === "question") {
3101
- const hasQuestionHeader = /Question \d+\/\d+:/i.test(recent);
3102
- const hasQuestionOptions = /^[>\s]*[1-9]\. \S/im.test(recent);
3103
- const hasQuestionFooter = /\benter Select\b/i.test(recent);
3104
- return hasQuestionHeader && hasQuestionOptions && hasQuestionFooter;
3383
+ const allLines = stripped.trimEnd().split("\n");
3384
+ const fenceMask = computeFenceMask(allLines);
3385
+ const windowStart = Math.max(0, allLines.length - 30);
3386
+ let headerIdx = -1;
3387
+ for (let i = allLines.length - 1; i >= windowStart; i--) {
3388
+ if (!fenceMask[i] && isQuestionHeaderLine(allLines[i])) {
3389
+ headerIdx = i;
3390
+ break;
3391
+ }
3392
+ }
3393
+ if (headerIdx < 0) return false;
3394
+ const headerLine = allLines[headerIdx].trim();
3395
+ const optionIndices = [];
3396
+ for (let i = headerIdx + 1; i < allLines.length; i++) {
3397
+ if (!fenceMask[i] && /^[>\s❯]*[1-9][.)]\s+\S/.test(allLines[i].trim())) {
3398
+ optionIndices.push(i);
3399
+ }
3400
+ }
3401
+ if (optionIndices.length < 2) return false;
3402
+ const lastOptionIdx = optionIndices[optionIndices.length - 1];
3403
+ const hasHeaderHint = /\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)/i.test(headerLine);
3404
+ let hasQuestionFooter = hasHeaderHint;
3405
+ let footerIdx = lastOptionIdx;
3406
+ if (!hasQuestionFooter) {
3407
+ for (let i = lastOptionIdx + 1; i < allLines.length; i++) {
3408
+ if (!fenceMask[i] && isQuestionFooterLine(allLines[i].trim())) {
3409
+ hasQuestionFooter = true;
3410
+ footerIdx = i;
3411
+ break;
3412
+ }
3413
+ }
3414
+ }
3415
+ if (!hasQuestionFooter) return false;
3416
+ if (hasTrailingAgyComposer(allLines, footerIdx, fenceMask)) {
3417
+ return false;
3418
+ }
3419
+ return true;
3105
3420
  }
3106
3421
  if (activeKind === "file_access") {
3107
- const hasFileAccessHeader = /^File access\s*$/im.test(recent);
3108
- const hasFileAccessQuestion = /Allow access to this file\?/i.test(recent);
3109
- const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(recent);
3110
- const hasApprovalFooter = /\besc to cancel\b/i.test(recent);
3111
- return hasFileAccessHeader && hasFileAccessQuestion && hasApprovalOptions && hasApprovalFooter;
3422
+ const hasFileAccessHeader = /^File access\s*$/im.test(recent) || /^\s*(?:Read|Write|Edit|Access):\s*\S/im.test(recent);
3423
+ const questionMatches = Array.from(recent.matchAll(/^\s*Allow access to this file\?\s*$/gim));
3424
+ if (!hasFileAccessHeader || questionMatches.length === 0) {
3425
+ return false;
3426
+ }
3427
+ const lastQuestionMatch = questionMatches[questionMatches.length - 1];
3428
+ if (lastQuestionMatch.index === void 0) {
3429
+ return false;
3430
+ }
3431
+ const belowQuestion = recent.slice(lastQuestionMatch.index);
3432
+ if (/\?\s+for shortcuts/i.test(belowQuestion)) {
3433
+ return false;
3434
+ }
3435
+ const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(belowQuestion);
3436
+ const hasApprovalFooter = /^\s*(?:esc to cancel\b|[↑↓/\s]*Navigate\b|ctrl\+r\s+Review\b)/im.test(belowQuestion);
3437
+ const hasQuestionFooter = /\benter\s+Select\b/i.test(belowQuestion);
3438
+ return hasApprovalOptions && hasApprovalFooter && !hasQuestionFooter;
3112
3439
  }
3113
3440
  return true;
3114
3441
  }
@@ -3119,21 +3446,62 @@ function looksLikePromptSnapshot(snapshot) {
3119
3446
  function extractFileAccessPrompt(snapshot) {
3120
3447
  const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3121
3448
  const lines = stripped.split("\n");
3122
- let headingIndex = -1;
3449
+ let questionIndex = -1;
3123
3450
  for (let index = lines.length - 1; index >= 0; index -= 1) {
3124
- if (/^\s*File access\s*$/i.test(lines[index])) {
3125
- headingIndex = index;
3451
+ if (/^\s*Allow access to this file\?\s*$/i.test(lines[index])) {
3452
+ questionIndex = index;
3126
3453
  break;
3127
3454
  }
3128
3455
  }
3129
- if (headingIndex < 0) return null;
3130
- const block = lines.slice(headingIndex, headingIndex + 20);
3131
- const questionIndex = block.findIndex((line) => /Allow access to this file\?/i.test(line));
3132
3456
  if (questionIndex < 0) return null;
3133
- const readLine = block.slice(0, questionIndex + 1).find((line) => /^\s*Read:\s*\S/i.test(line));
3134
- const identity = readLine?.replace(/^\s*Read:\s*/i, "").trim() || "non-workspace file access";
3135
- const fullLine = block[questionIndex].trim();
3136
- const body = block.slice(1, questionIndex).join("\n").trim();
3457
+ const scanStart = Math.max(0, questionIndex - 15);
3458
+ let headingIndex = -1;
3459
+ let actionIndex = -1;
3460
+ let actionLine = null;
3461
+ let targetPath = null;
3462
+ for (let index = questionIndex - 1; index >= scanStart; index -= 1) {
3463
+ const line = lines[index].trim();
3464
+ if (!line) continue;
3465
+ const actionMatch = line.match(/^\s*((?:Read|Write|Edit|Access):\s*(\S.*))$/i);
3466
+ if (actionMatch && actionIndex < 0) {
3467
+ actionIndex = index;
3468
+ actionLine = actionMatch[1].trim();
3469
+ targetPath = actionMatch[2].trim();
3470
+ for (let prevIdx = index - 1; prevIdx >= scanStart; prevIdx -= 1) {
3471
+ const prevLine = lines[prevIdx].trim();
3472
+ if (!prevLine) continue;
3473
+ if (/^\s*File access\s*$/i.test(prevLine)) {
3474
+ headingIndex = prevIdx;
3475
+ }
3476
+ break;
3477
+ }
3478
+ break;
3479
+ }
3480
+ if (/^\s*Reason:\s*\S.*/i.test(line) && actionIndex < 0) {
3481
+ continue;
3482
+ }
3483
+ if (/^[⚠⚠️]\s*/.test(line) && actionIndex < 0) {
3484
+ continue;
3485
+ }
3486
+ if (/^\s*File access\s*$/i.test(line)) {
3487
+ headingIndex = index;
3488
+ break;
3489
+ }
3490
+ if (/^[─━│╭╰╮╯\s_-]*$/.test(line) || /^[│\s]*[❯>]\s+\S/.test(line) || /^\s*(?:Requesting permission for:|Question \d+\/\d+:|Allow access to this file\?|Do you want to proceed\?)/i.test(line) || /^[▶▸\s]*Thought|^●\s*/i.test(line) || /^\s*(?:\?\s+for shortcuts|esc to cancel)\b/i.test(line) || /^\s*(?:[↑↓/\s]*Navigate\b|ctrl\+r\s+Review\b)/i.test(line)) {
3491
+ break;
3492
+ }
3493
+ break;
3494
+ }
3495
+ const identity = targetPath || "non-workspace file access";
3496
+ const bodyStart = headingIndex >= 0 ? headingIndex + 1 : actionIndex >= 0 ? actionIndex : questionIndex;
3497
+ const contextLines = lines.slice(bodyStart, questionIndex).map((line) => line.trim()).filter((line) => {
3498
+ if (!line) return false;
3499
+ if (/^\s*File access\s*$/i.test(line)) return false;
3500
+ if (actionIndex < 0 && /^\s*Reason:\s*/i.test(line)) return false;
3501
+ return true;
3502
+ });
3503
+ const body = contextLines.join("\n").trim();
3504
+ const fullLine = actionLine ? `Allow access to this file? (${actionLine})` : lines[questionIndex].trim();
3137
3505
  return { identity, fullLine, body };
3138
3506
  }
3139
3507
  function extractHeader(snapshot) {
@@ -3148,13 +3516,177 @@ function extractFullHeaderLine(snapshot) {
3148
3516
  if (matches.length === 0) return null;
3149
3517
  return matches[matches.length - 1][0].trim();
3150
3518
  }
3519
+ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3520
+ let activeFence = null;
3521
+ for (let k = checkTop; k < topAnchor; k++) {
3522
+ const raw = lines[k];
3523
+ if (!activeFence) {
3524
+ const openMatch = raw.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
3525
+ if (openMatch) {
3526
+ const fenceStr = openMatch[1];
3527
+ const rest = openMatch[2];
3528
+ const char = fenceStr[0];
3529
+ if (char === "`" && rest.includes("`")) {
3530
+ } else {
3531
+ activeFence = { char, length: fenceStr.length };
3532
+ }
3533
+ }
3534
+ } else {
3535
+ const closeMatch = raw.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
3536
+ if (closeMatch) {
3537
+ const fenceStr = closeMatch[1];
3538
+ if (fenceStr[0] === activeFence.char && fenceStr.length >= activeFence.length) {
3539
+ activeFence = null;
3540
+ }
3541
+ }
3542
+ }
3543
+ }
3544
+ if (activeFence !== null) {
3545
+ return true;
3546
+ }
3547
+ for (let k = boundaryIdx - 1; k >= checkTop; k--) {
3548
+ const l = lines[k].trim();
3549
+ if (!l) continue;
3550
+ if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /Question \d+\/\d+:/i.test(l)) {
3551
+ const sliceBetween = lines.slice(k + 1, boundaryIdx);
3552
+ let optCount = 0;
3553
+ let hasFooter = false;
3554
+ let inDiff = false;
3555
+ for (const raw of sliceBetween) {
3556
+ const s = raw.trim();
3557
+ if (!s) continue;
3558
+ if (inDiff) {
3559
+ if (/^[ +\-\\]/.test(raw)) {
3560
+ continue;
3561
+ }
3562
+ if (/^(?:diff\s+--git\b|@@\s+-\d+|---\s+(?:[ab]\/|\/dev\/null)|\+\+\+\s+(?:[ab]\/|\/dev\/null)|index\s+[0-9a-f]{7,})/i.test(s)) {
3563
+ continue;
3564
+ }
3565
+ if (/^●\s+(?:Edit completed|[A-Z])/i.test(raw)) {
3566
+ inDiff = false;
3567
+ continue;
3568
+ }
3569
+ if (/^Do you want to proceed\?/i.test(raw)) {
3570
+ inDiff = false;
3571
+ continue;
3572
+ }
3573
+ inDiff = false;
3574
+ }
3575
+ if (/^(?:diff\s+--git\b|@@\s+-\d+|---\s+(?:[ab]\/|\/dev\/null)|\+\+\+\s+(?:[ab]\/|\/dev\/null)|index\s+[0-9a-f]{7,})/i.test(s)) {
3576
+ inDiff = true;
3577
+ continue;
3578
+ }
3579
+ if (/^●\s+(?:Edit completed|[A-Z])/i.test(raw)) {
3580
+ continue;
3581
+ }
3582
+ if (/^[>\s]*[1-9]\.\s+\S/.test(s)) optCount++;
3583
+ if (/esc to cancel\b|enter Select\b|[↑↓/\s]*Navigate\b/i.test(s)) hasFooter = true;
3584
+ }
3585
+ if (optCount < 2 || !hasFooter) {
3586
+ return true;
3587
+ }
3588
+ }
3589
+ }
3590
+ return false;
3591
+ }
3592
+ function isStandaloneFileAccessContext(lines, questionIdx) {
3593
+ const windowEnd = Math.min(lines.length, questionIdx + 15);
3594
+ let hasFileAccessOption = false;
3595
+ for (let j = questionIdx + 1; j < windowEnd; j++) {
3596
+ const l = lines[j].trim();
3597
+ if (!l) continue;
3598
+ if (/\benter\s+Select\b/i.test(l) || /\benter\s+to\s+(?:select|submit)\b/i.test(l)) {
3599
+ return false;
3600
+ }
3601
+ if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l)) {
3602
+ return false;
3603
+ }
3604
+ if (/\b(?:allow|deny)\s+access\b/i.test(l)) {
3605
+ hasFileAccessOption = true;
3606
+ }
3607
+ if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
3608
+ break;
3609
+ }
3610
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l)) {
3611
+ break;
3612
+ }
3613
+ }
3614
+ const start = Math.max(0, questionIdx - 15);
3615
+ let actionIdx = -1;
3616
+ let headingIdx = -1;
3617
+ for (let j = questionIdx - 1; j >= start; j--) {
3618
+ const l = lines[j].trim();
3619
+ if (!l) continue;
3620
+ if (/^(?:diff\s+--git|index\s+[0-9a-f]{7,}|---\s+(?:[ab]\/|\/dev\/null)|\+\+\+\s+(?:[ab]\/|\/dev\/null)|@@\s+-\d+)/i.test(l) || /^ {0,3}(`{3,}|~{3,})(?:[a-z0-9_+-]+)?\s*$/i.test(l) || /Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /Question \d+\/\d+:/i.test(l)) {
3621
+ return false;
3622
+ }
3623
+ if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
3624
+ break;
3625
+ }
3626
+ if (/^[│\s]*[❯>]\s*$/.test(l)) {
3627
+ break;
3628
+ }
3629
+ if (/^\s*(?:Read|Write|Edit|Access):\s*\S/i.test(l)) {
3630
+ actionIdx = j;
3631
+ for (let k = j - 1; k >= start; k--) {
3632
+ const prev = lines[k].trim();
3633
+ if (!prev) continue;
3634
+ if (/^\s*File access\s*$/i.test(prev)) {
3635
+ headingIdx = k;
3636
+ }
3637
+ break;
3638
+ }
3639
+ break;
3640
+ }
3641
+ if (/^\s*Reason:\s*\S/i.test(l)) {
3642
+ continue;
3643
+ }
3644
+ if (/^[⚠⚠️]\s*/.test(l)) {
3645
+ continue;
3646
+ }
3647
+ if (/^\s*File access\s*$/i.test(l)) {
3648
+ headingIdx = j;
3649
+ break;
3650
+ }
3651
+ return false;
3652
+ }
3653
+ if (actionIdx < 0 && headingIdx < 0) {
3654
+ return false;
3655
+ }
3656
+ if (headingIdx < 0 && !hasFileAccessOption) {
3657
+ return false;
3658
+ }
3659
+ const topAnchor = headingIdx >= 0 ? headingIdx : actionIdx;
3660
+ const checkTop = Math.max(0, topAnchor - 25);
3661
+ for (let j = topAnchor - 1; j >= checkTop; j--) {
3662
+ const l = lines[j].trim();
3663
+ if (!l) continue;
3664
+ if (/Question \d+\/\d+:/i.test(l)) {
3665
+ return false;
3666
+ }
3667
+ if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l)) {
3668
+ return false;
3669
+ }
3670
+ if (/^(?:diff\s+--git|index\s+[0-9a-f]{7,}|---\s+(?:[ab]\/|\/dev\/null)|\+\+\+\s+(?:[ab]\/|\/dev\/null)|@@\s+-\d+)/i.test(l) || /^ {0,3}(`{3,}|~{3,})(?:[a-z0-9_+-]+)?\s*$/i.test(l)) {
3671
+ return false;
3672
+ }
3673
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l) || /^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
3674
+ if (isOpenEnclosingContext(lines, checkTop, topAnchor, j)) {
3675
+ return false;
3676
+ }
3677
+ break;
3678
+ }
3679
+ }
3680
+ return true;
3681
+ }
3151
3682
  function detectActivePromptKind(snapshot) {
3152
3683
  const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3153
3684
  const lines = stripped.trimEnd().split("\n");
3154
3685
  const windowStart = Math.max(0, lines.length - 30);
3155
3686
  const approvalRe = /Requesting permission for:/i;
3156
- const questionRe = /Question \d+\/\d+:/i;
3157
3687
  const fileAccessRe = /^\s*File access\s*$/i;
3688
+ const fileAccessQuestionRe = /^\s*Allow access to this file\?\s*$/i;
3689
+ const lineInFence = computeFenceMask(lines);
3158
3690
  let lastApprovalLineIdx = -1;
3159
3691
  let lastQuestionLineIdx = -1;
3160
3692
  let lastFileAccessLineIdx = -1;
@@ -3162,14 +3694,22 @@ function detectActivePromptKind(snapshot) {
3162
3694
  for (let i = lines.length - 1; i >= 0; i--) {
3163
3695
  const line = lines[i];
3164
3696
  if (i >= windowStart) {
3165
- if (lastApprovalLineIdx < 0 && approvalRe.test(line)) {
3166
- lastApprovalLineIdx = i;
3697
+ if (approvalRe.test(line)) {
3698
+ if (!lineInFence[i] && lastApprovalLineIdx < 0) {
3699
+ lastApprovalLineIdx = i;
3700
+ }
3167
3701
  }
3168
- if (lastQuestionLineIdx < 0 && questionRe.test(line)) {
3169
- lastQuestionLineIdx = i;
3702
+ if (isQuestionHeaderLine(line)) {
3703
+ if (!lineInFence[i] && lastQuestionLineIdx < 0) {
3704
+ lastQuestionLineIdx = i;
3705
+ }
3170
3706
  }
3171
- if (lastFileAccessLineIdx < 0 && fileAccessRe.test(line)) {
3172
- lastFileAccessLineIdx = i;
3707
+ if (lastFileAccessLineIdx < 0 && !lineInFence[i]) {
3708
+ if (fileAccessRe.test(line)) {
3709
+ lastFileAccessLineIdx = i;
3710
+ } else if (fileAccessQuestionRe.test(line) && isStandaloneFileAccessContext(lines, i)) {
3711
+ lastFileAccessLineIdx = i;
3712
+ }
3173
3713
  }
3174
3714
  }
3175
3715
  if (lastSystemErrorLineIdx < 0 && isSystemErrorBannerLineInContext(lines, i)) {
@@ -3180,12 +3720,14 @@ function detectActivePromptKind(snapshot) {
3180
3720
  if (lastApprovalLineIdx < 0 && lastQuestionLineIdx < 0) {
3181
3721
  for (let i = lines.length - 1; i >= windowStart; i--) {
3182
3722
  if (/Do you want to proceed\?/i.test(lines[i])) {
3183
- lastApprovalLineIdx = i;
3184
- break;
3723
+ if (!lineInFence[i]) {
3724
+ lastApprovalLineIdx = i;
3725
+ break;
3726
+ }
3185
3727
  }
3186
3728
  }
3187
3729
  }
3188
- const latestChooserIdx = Math.max(
3730
+ let latestChooserIdx = Math.max(
3189
3731
  lastApprovalLineIdx,
3190
3732
  lastQuestionLineIdx,
3191
3733
  lastFileAccessLineIdx
@@ -3194,15 +3736,32 @@ function detectActivePromptKind(snapshot) {
3194
3736
  if (latestChooserIdx >= 0) {
3195
3737
  const remainingLines = lines.slice(lastSystemErrorLineIdx + 1).join("\n");
3196
3738
  if (latestChooserIdx === lastQuestionLineIdx) {
3197
- const hasQuestionOptions = /^[>\s]*[1-9]\. \S/im.test(remainingLines);
3198
- const hasQuestionFooter = /\benter Select\b/i.test(remainingLines);
3199
- if (hasQuestionOptions && hasQuestionFooter) {
3739
+ let hasQuestionOptions = false;
3740
+ let hasQuestionFooter = false;
3741
+ let promptEndIdx = lastSystemErrorLineIdx;
3742
+ for (let j = lastSystemErrorLineIdx + 1; j < lines.length; j++) {
3743
+ if (!lineInFence[j]) {
3744
+ const trimmed = lines[j].trim();
3745
+ if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
3746
+ hasQuestionOptions = true;
3747
+ promptEndIdx = j;
3748
+ } else if (isQuestionFooterLine(lines[j])) {
3749
+ hasQuestionFooter = true;
3750
+ promptEndIdx = j;
3751
+ }
3752
+ }
3753
+ }
3754
+ const headerLine = lines[lastQuestionLineIdx].trim();
3755
+ const hasHeaderHint = /\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)/i.test(headerLine);
3756
+ const footerSatisfied = hasQuestionFooter || hasHeaderHint;
3757
+ const hasComposer = hasTrailingAgyComposer(lines, promptEndIdx, lineInFence);
3758
+ if (hasQuestionOptions && footerSatisfied && !hasComposer) {
3200
3759
  return "question";
3201
3760
  }
3202
3761
  } else if (latestChooserIdx === lastFileAccessLineIdx) {
3203
- const hasFileAccessQuestion = /Allow access to this file\?/i.test(remainingLines);
3762
+ const hasFileAccessQuestion = /^\s*Allow access to this file\?\s*$/im.test(remainingLines);
3204
3763
  const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(remainingLines);
3205
- const hasApprovalFooter = /\besc to cancel\b/i.test(remainingLines);
3764
+ const hasApprovalFooter = /^\s*(?:esc to cancel\b|[↑↓/\s]*Navigate\b|ctrl\+r\s+Review\b)/im.test(remainingLines);
3206
3765
  if (hasFileAccessQuestion && hasApprovalOptions && hasApprovalFooter) {
3207
3766
  return "file_access";
3208
3767
  }
@@ -3224,6 +3783,20 @@ function detectActivePromptKind(snapshot) {
3224
3783
  return "file_access";
3225
3784
  }
3226
3785
  if (latestChooserIdx === lastQuestionLineIdx) {
3786
+ let promptEndIdx = lastQuestionLineIdx;
3787
+ for (let j = lastQuestionLineIdx + 1; j < lines.length; j++) {
3788
+ if (!lineInFence[j]) {
3789
+ if (isAgyStatusFooterLine(lines[j].trim()) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(lines[j].trim()) || isComposerBoxAt(lines, j)) {
3790
+ break;
3791
+ }
3792
+ if (/^[>\s❯]*[1-9][.)]\s+\S/.test(lines[j].trim()) || isQuestionFooterLine(lines[j])) {
3793
+ promptEndIdx = j;
3794
+ }
3795
+ }
3796
+ }
3797
+ if (hasTrailingAgyComposer(lines, promptEndIdx, lineInFence)) {
3798
+ return null;
3799
+ }
3227
3800
  return "question";
3228
3801
  }
3229
3802
  return "approval";
@@ -3574,12 +4147,45 @@ function extractSystemErrorPrompt(snapshot) {
3574
4147
  const fullLine = errorId ? `\u26A0\uFE0F Antigravity error: ${message} (Error ID: ${errorId})` : `\u26A0\uFE0F Antigravity error: ${message}`;
3575
4148
  return { message, errorId, fullLine };
3576
4149
  }
4150
+ function isQuestionHeaderLine(line) {
4151
+ if (!line) return false;
4152
+ const trimmed = line.trim();
4153
+ if (isAgyStatusFooterLine(trimmed)) return false;
4154
+ if (/^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
4155
+ return false;
4156
+ }
4157
+ return /^\s*(?:[❯>]\s*)?\?\s+(?!for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b)\S/i.test(trimmed) || /^\s*(?:[❯>]\s*)?(?:\?\s*)?Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*\S/i.test(trimmed);
4158
+ }
4159
+ function isQuestionFooterLine(line) {
4160
+ if (!line) return false;
4161
+ const trimmed = line.trim();
4162
+ if (/^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
4163
+ return false;
4164
+ }
4165
+ if (isComposedStatusFooter(trimmed)) return false;
4166
+ if (/^\s*esc to cancel\b/i.test(trimmed) && /(?:Gemini|Flash|Pro|Claude|Sonnet|Opus|GPT|auto|high|low|medium|credits|tasks|subagents?|quota)/i.test(trimmed)) {
4167
+ return false;
4168
+ }
4169
+ if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) return false;
4170
+ if (isQuestionHeaderLine(trimmed)) return false;
4171
+ return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?select)?$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?select(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|to select))?(?:\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) || /^waiting(?:\.\.\.)?$/i.test(trimmed);
4172
+ }
3577
4173
  function extractQuestionHeader(snapshot) {
3578
4174
  const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3579
- const matches = Array.from(stripped.matchAll(/Question \d+\/\d+:\s*([^\n]+)/gi));
3580
- if (matches.length === 0) return null;
3581
- const last = matches[matches.length - 1];
3582
- return { fullLine: last[0].trim(), body: last[1].trim() };
4175
+ const lines = stripped.split("\n");
4176
+ const lineInFence = computeFenceMask(lines);
4177
+ const prefixRe = /^\s*(?:[❯>]\s*)?(?:\?\s*(?:Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)?|Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)/i;
4178
+ for (let i = lines.length - 1; i >= 0; i--) {
4179
+ if (lineInFence[i]) continue;
4180
+ const line = lines[i];
4181
+ if (isQuestionHeaderLine(line)) {
4182
+ const fullLine = line.trim();
4183
+ let body = fullLine.replace(prefixRe, "").trim();
4184
+ body = body.replace(/\s*\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)\s*$/i, "").trim();
4185
+ return { fullLine, body };
4186
+ }
4187
+ }
4188
+ return null;
3583
4189
  }
3584
4190
  function hashPromptSnapshot(snapshot) {
3585
4191
  let normalized = snapshot.replace(ANSI_ESCAPE_REGEX, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
@@ -3599,8 +4205,7 @@ function sleep(ms) {
3599
4205
  });
3600
4206
  }
3601
4207
  function isQuestionUISnapshot(snapshot) {
3602
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3603
- return /Question \d+\/\d+:/i.test(stripped);
4208
+ return extractQuestionHeader(snapshot) !== null;
3604
4209
  }
3605
4210
 
3606
4211
  // src/approval-detector.ts
@@ -3656,7 +4261,7 @@ function parseFileAccessSnapshot(stripped, originalSnapshot, prompt) {
3656
4261
  const lines = stripped.split("\n");
3657
4262
  let questionIndex = -1;
3658
4263
  for (let index = lines.length - 1; index >= 0; index -= 1) {
3659
- if (/Allow access to this file\?/i.test(lines[index])) {
4264
+ if (/^\s*Allow access to this file\?\s*$/i.test(lines[index])) {
3660
4265
  questionIndex = index;
3661
4266
  break;
3662
4267
  }
@@ -3721,9 +4326,11 @@ function parseApprovalUISnapshot(stripped, originalSnapshot) {
3721
4326
  }
3722
4327
  function parseQuestionSnapshot(stripped, originalSnapshot, header) {
3723
4328
  const lines = stripped.split("\n");
4329
+ const lineInFence = computeFenceMask(lines);
3724
4330
  let headerIdx = -1;
3725
4331
  for (let i = lines.length - 1; i >= 0; i--) {
3726
- if (/Question \d+\/\d+:/i.test(lines[i])) {
4332
+ if (lineInFence[i]) continue;
4333
+ if (isQuestionHeaderLine(lines[i])) {
3727
4334
  headerIdx = i;
3728
4335
  break;
3729
4336
  }
@@ -3731,7 +4338,10 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
3731
4338
  const belowHeader = headerIdx >= 0 ? lines.slice(headerIdx + 1).join("\n") : stripped;
3732
4339
  const options = parseOptions(belowHeader);
3733
4340
  if (options.length < 2) return null;
3734
- const submitMap = buildQuestionSubmitMap(options);
4341
+ const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
4342
+ const hasArrowKeyHint = /\((?:Use\s+arrow\s+keys|Use\s+\S+\s+keys)[^)]*\)/i.test(normalizedHeader);
4343
+ const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !hasArrowKeyHint;
4344
+ const submitMap = isBaseNumberedWizard ? buildQuestionSubmitMap(options) : {};
3735
4345
  const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
3736
4346
  return {
3737
4347
  kind: "question",
@@ -3747,16 +4357,11 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
3747
4357
  }
3748
4358
  function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
3749
4359
  const lines = belowHeader.split("\n");
3750
- const optionRegex = /^[>\s]*([1-9])\. (.+?)\s*$/;
3751
- const fenceRegex = /^```/;
4360
+ const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
4361
+ const lineInFence = computeFenceMask(lines);
3752
4362
  let firstOptionIdx = -1;
3753
- let inFence = false;
3754
4363
  for (let i = 0; i < lines.length; i++) {
3755
- if (fenceRegex.test(lines[i])) {
3756
- inFence = !inFence;
3757
- continue;
3758
- }
3759
- if (inFence) continue;
4364
+ if (lineInFence[i]) continue;
3760
4365
  if (optionRegex.test(lines[i])) {
3761
4366
  firstOptionIdx = i;
3762
4367
  break;
@@ -3766,8 +4371,19 @@ function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
3766
4371
  return lines.slice(0, firstOptionIdx).join("\n").trim();
3767
4372
  }
3768
4373
  function parseOptions(snapshot) {
3769
- const recent = snapshot.split("\n").slice(-30);
3770
- const optionRegex = /^[>\s]*([1-9])\. (.+?)\s*$/;
4374
+ const lines = snapshot.split("\n");
4375
+ const lineInFence = computeFenceMask(lines);
4376
+ const filteredLines = [];
4377
+ for (let i = 0; i < lines.length; i++) {
4378
+ if (!lineInFence[i]) {
4379
+ filteredLines.push(lines[i]);
4380
+ }
4381
+ }
4382
+ while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === "") {
4383
+ filteredLines.pop();
4384
+ }
4385
+ const recent = filteredLines.slice(-30);
4386
+ const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
3771
4387
  const found = /* @__PURE__ */ new Map();
3772
4388
  for (const line of recent) {
3773
4389
  const m = line.match(optionRegex);
@@ -3871,6 +4487,7 @@ function extractIdentity(headerText) {
3871
4487
  return { command: trimmed };
3872
4488
  }
3873
4489
  function guessToolTypeFromIdentity(c) {
4490
+ if (c.kind === "question") return "ASK_QUESTION";
3874
4491
  if (c.command) return "RUN_COMMAND";
3875
4492
  if (c.filePath) return "CODE_ACTION";
3876
4493
  return "GENERIC";
@@ -3977,11 +4594,12 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
3977
4594
  intentByteOffset: 0,
3978
4595
  // Best-effort tool-type inference from the candidate. For shell
3979
4596
  // commands → RUN_COMMAND. For edit-style headers with a file
3980
- // path → CODE_ACTION. Default: GENERIC.
4597
+ // path → CODE_ACTION. For questions → ASK_QUESTION. Default: GENERIC.
3981
4598
  toolType: guessToolTypeFromIdentity(candidate),
3982
4599
  command: candidate.command,
3983
4600
  filePath: candidate.filePath,
3984
- rawToolName: candidate.command ?? candidate.filePath ?? "unknown",
4601
+ rawToolName: candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown",
4602
+ questionText: candidate.kind === "question" ? candidate.headerText : void 0,
3985
4603
  promptId,
3986
4604
  startedAt: Date.now(),
3987
4605
  intentCreatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4303,6 +4921,21 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
4303
4921
  return `${call.conversationId}|${call.intentStepIndex}|${call.toolCallIndex}|${call.intentByteOffset}`;
4304
4922
  }
4305
4923
  findMatchingPendingCall(candidate, conversationId) {
4924
+ if (candidate.kind === "question") {
4925
+ const questionCalls = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter(
4926
+ (c) => c.toolType === "ASK_QUESTION" || c.rawToolName?.toLowerCase() === "ask_question"
4927
+ ).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
4928
+ if (questionCalls.length > 0 && candidate.headerText) {
4929
+ const normHeader = candidate.headerText.toLowerCase().trim();
4930
+ const textMatch = questionCalls.find((c) => {
4931
+ if (!c.questionText) return false;
4932
+ const normQ = c.questionText.toLowerCase().trim();
4933
+ return normHeader === normQ || normHeader.includes(normQ) || normQ.includes(normHeader);
4934
+ });
4935
+ if (textMatch) return textMatch;
4936
+ }
4937
+ return null;
4938
+ }
4306
4939
  if (candidate.command) {
4307
4940
  const matches = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.command && c.command === candidate.command).sort((a, b) => a.intentStepIndex - b.intentStepIndex);
4308
4941
  if (matches.length > 0) return matches[0];
@@ -4605,6 +5238,33 @@ var PromptResponder = class {
4605
5238
  const before = await this.paneObserver.probeApprovalUIActive();
4606
5239
  if (!before.probeSucceeded) return { ok: false, reason: "prompt-probe-failed" };
4607
5240
  if (!before.active) return { ok: true };
5241
+ if (before.ambiguous) {
5242
+ return {
5243
+ ok: false,
5244
+ reason: "ambiguous-prompt-active",
5245
+ details: "masked/ambiguous prompt cannot be dismissed via Escape from mobile"
5246
+ };
5247
+ }
5248
+ if (before.kind === "question") {
5249
+ let isActionableWizard = false;
5250
+ try {
5251
+ const snapshot = await this.paneObserver.captureSnapshot();
5252
+ if (snapshot) {
5253
+ const parsed = parseApprovalSnapshot(snapshot);
5254
+ if (parsed && parsed.kind === "question" && Object.keys(parsed.submitMap || {}).length > 0) {
5255
+ isActionableWizard = true;
5256
+ }
5257
+ }
5258
+ } catch {
5259
+ }
5260
+ if (!isActionableWizard) {
5261
+ return {
5262
+ ok: false,
5263
+ reason: "display-only-question-active",
5264
+ details: "active question prompt cannot be dismissed via Escape from mobile"
5265
+ };
5266
+ }
5267
+ }
4608
5268
  await this.sendKey(target, "Escape");
4609
5269
  await delay(250);
4610
5270
  const after = await this.paneObserver.probeApprovalUIActive();
@@ -4981,6 +5641,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
4981
5641
  conversationId: emit.conversationId,
4982
5642
  command: extractCommandFromArgs(name, args),
4983
5643
  filePath: extractFilePathFromArgs(name, args),
5644
+ questionText: extractQuestionFromArgs(name, args),
4984
5645
  cwd: extractCwdFromArgs(args),
4985
5646
  rawToolName: name,
4986
5647
  startedAt: Date.now(),
@@ -5082,8 +5743,9 @@ function hookToolNameToTranscriptType(hookName) {
5082
5743
  case "MANAGE_TASK":
5083
5744
  case "SCHEDULE":
5084
5745
  case "ASK_PERMISSION":
5085
- case "ASK_QUESTION":
5086
5746
  return "GENERIC";
5747
+ case "ASK_QUESTION":
5748
+ return "ASK_QUESTION";
5087
5749
  default:
5088
5750
  return upper;
5089
5751
  }
@@ -5119,6 +5781,32 @@ function extractFilePathFromArgs(toolName, args) {
5119
5781
  }
5120
5782
  return void 0;
5121
5783
  }
5784
+ function extractQuestionFromArgs(toolName, args) {
5785
+ if (!args || typeof args !== "object") return void 0;
5786
+ const tn = (toolName || "").toLowerCase().trim();
5787
+ const isQuestionTool = tn === "ask_question" || tn === "question" || tn === "ask";
5788
+ if (!isQuestionTool) return void 0;
5789
+ const a = args;
5790
+ if (typeof a.question === "string" && a.question.trim().length > 0) {
5791
+ return stripWrappingQuotes2(a.question.trim());
5792
+ }
5793
+ if (typeof a.prompt === "string" && a.prompt.trim().length > 0) {
5794
+ return stripWrappingQuotes2(a.prompt.trim());
5795
+ }
5796
+ if (Array.isArray(a.questions) && a.questions.length > 0) {
5797
+ const first = a.questions[0];
5798
+ if (typeof first === "string" && first.trim().length > 0) {
5799
+ return stripWrappingQuotes2(first.trim());
5800
+ }
5801
+ if (first && typeof first === "object") {
5802
+ const q = first.question;
5803
+ if (typeof q === "string" && q.trim().length > 0) {
5804
+ return stripWrappingQuotes2(q.trim());
5805
+ }
5806
+ }
5807
+ }
5808
+ return void 0;
5809
+ }
5122
5810
  function extractCwdFromArgs(args) {
5123
5811
  if (!args || typeof args !== "object") return void 0;
5124
5812
  const a = args;
@@ -5803,6 +6491,8 @@ var McpServer = class _McpServer {
5803
6491
  () => reject(new Error("status read timed out")),
5804
6492
  { once: true }
5805
6493
  ));
6494
+ timeout.catch(() => {
6495
+ });
5806
6496
  const subscription = await Promise.race([this.appSyncClient.getSubscriptionStatus(signal), timeout]);
5807
6497
  if (this.appSyncClient.getCurrentUserId() !== statusUserId) throw new Error("account changed during read");
5808
6498
  const sync = await Promise.race([this.appSyncClient.getCompanionSyncState(signal), timeout]);
@@ -7409,7 +8099,8 @@ var McpServer = class _McpServer {
7409
8099
  this.raiseSuppressedBadge(session, cand.snapshotHash);
7410
8100
  return;
7411
8101
  }
7412
- const e1rec = this.newE1Raise(session.sessionId, true);
8102
+ const isActionable = Object.keys(parsed.submitMap || {}).length > 0;
8103
+ const e1rec = this.newE1Raise(session.sessionId, isActionable);
7413
8104
  const emitted = this.approvalDetector.emitPaneOnlyPrompt(parsed, activeConvId, e1rec.promptId);
7414
8105
  if (emitted) {
7415
8106
  if (cand.snapshotHash) {
@@ -7458,10 +8149,11 @@ var McpServer = class _McpServer {
7458
8149
  });
7459
8150
  return;
7460
8151
  }
8152
+ const isDisplayOnly = Object.keys(state.submitMap || {}).length === 0;
7461
8153
  const e1existing = this.e1PromptRaises.get(state.promptId);
7462
- const e1rec = e1existing ? e1existing.record : this.newE1Raise(session.sessionId, true);
8154
+ const e1rec = e1existing ? e1existing.record : this.newE1Raise(session.sessionId, !isDisplayOnly);
7463
8155
  if (!e1existing) state.promptId = e1rec.promptId;
7464
- const optionsArr = state.paneOptions ? state.paneOptions.map((o) => ({ number: o.number, text: o.text })) : Object.keys(state.submitMap).map((n) => ({ number: n }));
8156
+ const optionsArr = isDisplayOnly ? [] : state.paneOptions ? state.paneOptions.map((o) => ({ number: o.number, text: o.text })) : Object.keys(state.submitMap).map((n) => ({ number: n }));
7465
8157
  const diffParsed = parseFencedDiffFromBody(
7466
8158
  state.body,
7467
8159
  state.paneDisplayHeader
@@ -7729,6 +8421,49 @@ var McpServer = class _McpServer {
7729
8421
  promptContent = intent.followUpText;
7730
8422
  await new Promise((resolve2) => setTimeout(resolve2, 250));
7731
8423
  } else {
8424
+ const hasDisplayOnlyPrompt = pendingPrompts.some(
8425
+ (p) => Object.keys(p.submitMap || {}).length === 0
8426
+ );
8427
+ let livePaneDisplayOnly = false;
8428
+ let livePaneBlockedReason = null;
8429
+ if (!hasDisplayOnlyPrompt && this.paneObserver) {
8430
+ try {
8431
+ const probe = await this.paneObserver.probeApprovalUIActive();
8432
+ if (!probe.probeSucceeded) {
8433
+ livePaneBlockedReason = "Live terminal state could not be verified safely. Please interact directly in your terminal.";
8434
+ } else if (probe.ambiguous) {
8435
+ livePaneBlockedReason = "An ambiguous prompt is active in your terminal. Please interact directly in your terminal.";
8436
+ } else if (probe.active && probe.kind === "question") {
8437
+ let isActionableWizard = false;
8438
+ try {
8439
+ const liveSnapshot = await this.paneObserver.captureSnapshot();
8440
+ if (liveSnapshot) {
8441
+ const parsed = parseApprovalSnapshot(liveSnapshot);
8442
+ if (parsed && parsed.kind === "question" && Object.keys(parsed.submitMap || {}).length > 0) {
8443
+ isActionableWizard = true;
8444
+ }
8445
+ }
8446
+ } catch {
8447
+ }
8448
+ if (!isActionableWizard) {
8449
+ livePaneDisplayOnly = true;
8450
+ }
8451
+ }
8452
+ } catch {
8453
+ livePaneBlockedReason = "Live terminal state could not be verified safely. Please interact directly in your terminal.";
8454
+ }
8455
+ }
8456
+ if (livePaneBlockedReason) {
8457
+ await this.emitPromptSafetyNotification(session, livePaneBlockedReason);
8458
+ return;
8459
+ }
8460
+ if (hasDisplayOnlyPrompt || livePaneDisplayOnly) {
8461
+ await this.emitPromptSafetyNotification(
8462
+ session,
8463
+ "The active terminal question prompt is display-only on mobile. Please select an option directly in your terminal."
8464
+ );
8465
+ return;
8466
+ }
7732
8467
  const dismissal = await this.promptResponder.dismissActivePrompt();
7733
8468
  if (!dismissal.ok) {
7734
8469
  await this.emitPromptSafetyNotification(
@@ -8357,16 +9092,19 @@ var __testing = {
8357
9092
  SessionNotFoundError,
8358
9093
  StoredAuthFailureError,
8359
9094
  TERMINAL_SHUTDOWN_SIGNALS,
9095
+ TmuxPaneObserver,
8360
9096
  __testing,
8361
9097
  classifyEntrypoint,
8362
9098
  classifyTmuxHasSessionError,
8363
9099
  commandDigest,
8364
9100
  detectActivePromptKind,
9101
+ extractFileAccessPrompt,
8365
9102
  extractSystemErrorPrompt,
8366
9103
  generateLaunchSessionId,
8367
9104
  getActiveConversationFromCliLog,
8368
9105
  isAgyStatusFooterLine,
8369
9106
  isComposedStatusFooter,
9107
+ isPaneInputSafetyBlocked,
8370
9108
  isSystemErrorActiveFromIndex,
8371
9109
  looksLikePromptSnapshot,
8372
9110
  parseApprovalSnapshot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.79",
3
+ "version": "2.0.81",
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": {
@@ -46,7 +46,7 @@
46
46
  "node": ">=22.0.0"
47
47
  },
48
48
  "dependencies": {
49
- "@quantiya/codevibe-core": "^2.0.73",
49
+ "@quantiya/codevibe-core": "^2.0.74",
50
50
  "chokidar": "^5.0.0",
51
51
  "dotenv": "^16.6.1",
52
52
  "express": "^5.1.0",