@quantiya/codevibe-antigravity-plugin 2.0.100 → 2.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agy-hook.js CHANGED
@@ -77,6 +77,7 @@ var FORWARDED_TOOL_ARG_KEYS = [
77
77
  "CommandLine",
78
78
  "Command",
79
79
  "command",
80
+ "Path",
80
81
  "AbsolutePath",
81
82
  "TargetFile",
82
83
  "FilePath",
package/dist/server.js CHANGED
@@ -43,6 +43,7 @@ __export(server_exports, {
43
43
  commandDigest: () => commandDigest,
44
44
  detectActivePromptKind: () => detectActivePromptKind,
45
45
  extractFileAccessPrompt: () => extractFileAccessPrompt,
46
+ extractFileEditPrompt: () => extractFileEditPrompt,
46
47
  extractSystemErrorPrompt: () => extractSystemErrorPrompt,
47
48
  generateLaunchSessionId: () => generateLaunchSessionId,
48
49
  getActiveConversationFromCliLog: () => getActiveConversationFromCliLog,
@@ -57,7 +58,8 @@ __export(server_exports, {
57
58
  readTranscriptFirstType: () => readTranscriptFirstType,
58
59
  reconstructFooter: () => reconstructFooter,
59
60
  runForeignSpawnIdle: () => runForeignSpawnIdle,
60
- safeIsoTimestamp: () => safeIsoTimestamp
61
+ safeIsoTimestamp: () => safeIsoTimestamp,
62
+ stripAnsiAndExpandRep: () => stripAnsiAndExpandRep
61
63
  });
62
64
  module.exports = __toCommonJS(server_exports);
63
65
  var crypto3 = __toESM(require("crypto"));
@@ -2326,6 +2328,7 @@ function inspectHookRegistration(paths) {
2326
2328
  // src/tool-prompt-match.ts
2327
2329
  var TOOL_COMMAND_KEYS = ["CommandLine", "Command", "command"];
2328
2330
  var TOOL_PATH_KEYS = [
2331
+ "Path",
2329
2332
  "AbsolutePath",
2330
2333
  "TargetFile",
2331
2334
  "FilePath",
@@ -2334,7 +2337,13 @@ var TOOL_PATH_KEYS = [
2334
2337
  "path",
2335
2338
  "DirectoryPath"
2336
2339
  ];
2337
- var EDIT_TOOLS = /* @__PURE__ */ new Set(["WRITE_TO_FILE", "REPLACE_FILE_CONTENT", "MULTI_REPLACE_FILE_CONTENT"]);
2340
+ var EDIT_TOOLS = /* @__PURE__ */ new Set([
2341
+ "EDIT_FILE",
2342
+ "WRITE_FILE",
2343
+ "WRITE_TO_FILE",
2344
+ "REPLACE_FILE_CONTENT",
2345
+ "MULTI_REPLACE_FILE_CONTENT"
2346
+ ]);
2338
2347
  function stripWrappingQuotes(value) {
2339
2348
  return value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
2340
2349
  }
@@ -2638,7 +2647,8 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2638
2647
  const err = extractSystemErrorPrompt(snapshot);
2639
2648
  headerText = err ? err.message : null;
2640
2649
  } else if (kind === "approval") {
2641
- headerText = extractHeader(snapshot);
2650
+ const fileEdit = extractFileEditPrompt(snapshot);
2651
+ headerText = fileEdit ? fileEdit.identity : extractHeader(snapshot);
2642
2652
  }
2643
2653
  }
2644
2654
  let isChooser = hasPrompt && kind !== "system_error";
@@ -2781,7 +2791,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2781
2791
  if (candidate.snapshotHash === this.lastPromptHash) return;
2782
2792
  this.lastPromptHash = candidate.snapshotHash;
2783
2793
  this.approvalUIActive = true;
2784
- this.currentApprovalHeader = extractHeader(candidate.snapshot);
2794
+ this.currentApprovalHeader = extractFileEditPrompt(candidate.snapshot)?.identity ?? extractHeader(candidate.snapshot);
2785
2795
  this.emit("prompt-candidate", candidate);
2786
2796
  }
2787
2797
  /**
@@ -2944,6 +2954,19 @@ function isComposerBoxAt(lines, idx) {
2944
2954
  return false;
2945
2955
  }
2946
2956
  var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
2957
+ var ANSI_NON_REP_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-ac-~])/g;
2958
+ function stripAnsiAndExpandRep(input) {
2959
+ if (!input.includes("\x1B")) return input;
2960
+ const withoutNonRep = input.replace(ANSI_NON_REP_ESCAPE_REGEX, "");
2961
+ const expanded = withoutNonRep.replace(
2962
+ /([^\x00-\x1f\x7f])\x1B\[(\d*)b/gu,
2963
+ (_, ch, countStr) => {
2964
+ const count = Math.min(Math.max(Number.parseInt(countStr || "1", 10) || 1, 1), 512);
2965
+ return ch.repeat(1 + count);
2966
+ }
2967
+ );
2968
+ return expanded.replace(ANSI_ESCAPE_REGEX, "");
2969
+ }
2947
2970
  function isPaneInputSafetyBlocked(input) {
2948
2971
  const rawText = Array.isArray(input) ? input.join("\n") : input;
2949
2972
  if (!rawText) return { blocked: false, kind: null };
@@ -2962,7 +2985,7 @@ function isPaneInputSafetyBlocked(input) {
2962
2985
  if (isQuestionFooterLine(trimmed)) {
2963
2986
  footerIdx = i;
2964
2987
  break;
2965
- } else if (/\besc to cancel\b/i.test(trimmed) && !isAgyStatusFooterLine(trimmed)) {
2988
+ } else if ((/\besc to cancel\b/i.test(trimmed) || /(?:↑\/↓|↑|↓)\s*Navigate\b.*\b(?:tab\s+Amend|f\s+full\s+diff|ctrl\+r\s+Review)\b/i.test(trimmed)) && !isAgyStatusFooterLine(trimmed)) {
2966
2989
  footerIdx = i;
2967
2990
  break;
2968
2991
  }
@@ -3048,7 +3071,7 @@ function isPaneInputSafetyBlocked(input) {
3048
3071
  candidateHeaders.push({ lineIdx: i, kind: "question" });
3049
3072
  } else if (/Allow access to this file\?/i.test(trimmed)) {
3050
3073
  candidateHeaders.push({ lineIdx: i, kind: "file_access" });
3051
- } else if (/Requesting permission for:/i.test(trimmed) || /Do you want to (?:proceed|run|execute)/i.test(trimmed)) {
3074
+ } else if (/Requesting permission for:/i.test(trimmed) || /Do you want to (?:proceed|run|execute)/i.test(trimmed) || /^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(trimmed)) {
3052
3075
  candidateHeaders.push({ lineIdx: i, kind: "approval" });
3053
3076
  }
3054
3077
  }
@@ -3116,7 +3139,7 @@ function looksLikePromptDelta(chunk) {
3116
3139
  const text = chunk.includes("\x1B") ? chunk.replace(ANSI_ESCAPE_REGEX, "") : chunk;
3117
3140
  return (
3118
3141
  // Approval UI signatures
3119
- /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)
3142
+ /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) || /^(?:Pending edit|Create file)\s*$/im.test(text) || /^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/im.test(text) || /(?:Yes, accept this change|No, reject this change|Yes, allow creation|No, deny creation)/i.test(text) || /(?:tab\s+Amend|f\s+full\s+diff)/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)
3120
3143
  );
3121
3144
  }
3122
3145
  function isNonSystemErrorWarningLine(trimmed) {
@@ -3152,7 +3175,7 @@ function isSystemErrorBannerLineInContext(lines, idx) {
3152
3175
  if (/^[─━│╭╰╮╯\s_-]*$/.test(nextTrimmed)) break;
3153
3176
  if (/^[│\s]*[❯>]/.test(nextTrimmed)) break;
3154
3177
  if (/^(?:⚠️|⚠\uFE0F?)\s+/.test(nextTrimmed)) break;
3155
- if (/Requesting permission for:|Question \d+\/\d+:|^\s*File access\s*$|^[>\s]*[1-9]\. \S/i.test(nextTrimmed)) break;
3178
+ if (/Requesting permission for:|Question \d+\/\d+:|^\s*(?:File access|Pending edit|Create file)\s*$|^[>\s]*[1-9]\. \S/i.test(nextTrimmed)) break;
3156
3179
  const idMatch = nextTrimmed.match(/^Error ID:\s*([0-9a-fA-F-]*)$/i);
3157
3180
  if (idMatch) {
3158
3181
  if (/[0-9a-fA-F]/.test(idMatch[1])) return true;
@@ -3272,7 +3295,7 @@ function hasTrailingAgyComposer(lines, afterLineIdx, fenceMask) {
3272
3295
  );
3273
3296
  if (hasStatusLine) {
3274
3297
  const nonDecorativeNonStatus = trailingLines.filter(
3275
- (l) => !isAgyStatusFooterLine(l) && !isQuestionFooterLine(l) && !/^[─━│╭╰╮╯\s_-]{3,}$/.test(l)
3298
+ (l) => !isAgyStatusFooterLine(l) && !isQuestionFooterLine(l) && !/(?:↑\/↓|↑|↓)\s*Navigate\b.*\b(?:tab\s+Amend|f\s+full\s+diff|ctrl\+r\s+Review)\b/i.test(l) && !/^[─━│╭╰╮╯\s_-]{3,}$/.test(l)
3276
3299
  );
3277
3300
  if (nonDecorativeNonStatus.length > 0) {
3278
3301
  return true;
@@ -3344,9 +3367,10 @@ function isSystemErrorActiveFromIndex(lines, errorIdx) {
3344
3367
  if (/^●\s*(?:[A-Za-z_-]+)\(/.test(trimmed)) return false;
3345
3368
  if (/Requesting permission for:/i.test(trimmed)) return false;
3346
3369
  if (/Question \d+\/\d+:/i.test(trimmed)) return false;
3347
- if (/^\s*File access\s*$/i.test(trimmed)) return false;
3370
+ if (/^\s*(?:File access|Pending edit|Create file)\s*$/i.test(trimmed)) return false;
3348
3371
  if (/^[>\s]*[1-9]\. \S/.test(trimmed)) return false;
3349
3372
  if (/Allow access to this file\?/i.test(trimmed)) return false;
3373
+ if (/^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(trimmed)) return false;
3350
3374
  if (/Do you want to proceed\?/i.test(trimmed)) return false;
3351
3375
  if (/\benter Select\b/i.test(trimmed)) return false;
3352
3376
  if (/^Error ID:\s*[0-9a-fA-F-]*/i.test(trimmed)) {
@@ -3420,12 +3444,15 @@ function isLegacyYNActiveInLines(lines) {
3420
3444
  return { active: true, lineIdx: ynIdx };
3421
3445
  }
3422
3446
  function looksLikePromptSnapshot(snapshot) {
3423
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3447
+ const stripped = stripAnsiAndExpandRep(snapshot);
3424
3448
  const activeKind = detectActivePromptKind(snapshot);
3425
3449
  if (activeKind !== null) {
3426
3450
  if (activeKind === "system_error") return true;
3427
3451
  const recent = stripped.trimEnd().split("\n").slice(-30).join("\n");
3428
3452
  if (activeKind === "approval") {
3453
+ if (extractFileEditPrompt(snapshot) !== null) {
3454
+ return true;
3455
+ }
3429
3456
  const hasApprovalHeader = /Requesting permission for:/i.test(recent) || /Do you want to proceed\?/i.test(recent);
3430
3457
  const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(recent);
3431
3458
  const hasApprovalFooter = /\besc to cancel\b/i.test(recent);
@@ -3562,14 +3589,333 @@ function extractFileAccessPrompt(snapshot) {
3562
3589
  const fullLine = actionLine ? `Allow access to this file? (${actionLine})` : lines[questionIndex].trim();
3563
3590
  return { identity, fullLine, body };
3564
3591
  }
3592
+ function parseInlineDiffLine(rawLine) {
3593
+ if (/^[>\s❯]*[1-9]\d*[.)]\s+/.test(rawLine)) {
3594
+ return null;
3595
+ }
3596
+ const remMatch = rawLine.match(/^\s*(\d+)\s{1,3}-(?:\s{1,2}(.*)|$)/);
3597
+ if (remMatch) {
3598
+ return {
3599
+ type: "remove",
3600
+ lineNum: Number.parseInt(remMatch[1], 10),
3601
+ content: remMatch[2] ?? ""
3602
+ };
3603
+ }
3604
+ const addMatch = rawLine.match(/^\s*(\d+)\s{1,3}\+(?:\s{1,2}(.*)|$)/);
3605
+ if (addMatch) {
3606
+ return {
3607
+ type: "add",
3608
+ lineNum: Number.parseInt(addMatch[1], 10),
3609
+ content: addMatch[2] ?? ""
3610
+ };
3611
+ }
3612
+ const ctxMatch = rawLine.match(/^\s*(\d+)(?:\s{5}(.*)|\s{3,4}(.*)|\s*)$/);
3613
+ if (ctxMatch) {
3614
+ return {
3615
+ type: "context",
3616
+ lineNum: Number.parseInt(ctxMatch[1], 10),
3617
+ content: ctxMatch[2] ?? ctxMatch[3] ?? ""
3618
+ };
3619
+ }
3620
+ return null;
3621
+ }
3622
+ function isFoldedDiffMarkerLine(trimmed) {
3623
+ return /^@@\s+\d+\s+lines\s+hidden\s+@@$/i.test(trimmed) || /^\(\d+\s+lines\s+hidden\)$/i.test(trimmed) || /^(?:…|\.\.\.)?\s*\d+\s+more\s+lines(?:\s*\(.*\))?$/i.test(trimmed);
3624
+ }
3625
+ function parseTargetStatSuffix(raw) {
3626
+ const m = raw.match(/^(.*?)\s+((?:\+(\d+)(?:\s+-(\d+))?|-(\d+)))\s*$/);
3627
+ if (!m) {
3628
+ return { cleanPath: raw.trim() };
3629
+ }
3630
+ const added = m[3] !== void 0 ? Number.parseInt(m[3], 10) : m[5] !== void 0 ? 0 : void 0;
3631
+ const removed = m[4] !== void 0 ? Number.parseInt(m[4], 10) : m[5] !== void 0 ? Number.parseInt(m[5], 10) : 0;
3632
+ return {
3633
+ cleanPath: m[1].trim(),
3634
+ stat: m[2].replace(/\s+/g, " ").trim(),
3635
+ added,
3636
+ removed
3637
+ };
3638
+ }
3639
+ function extractFileEditTargetAndStat(line) {
3640
+ if (parseInlineDiffLine(line) !== null) return null;
3641
+ const trimmed = line.trim();
3642
+ if (!trimmed) return null;
3643
+ const actionMatch = trimmed.match(/^(?:New file|Create file|Delete file|Write|Edit|Create):\s*(\S.*)$/i);
3644
+ if (actionMatch) {
3645
+ const parsed2 = parseTargetStatSuffix(actionMatch[1]);
3646
+ if (!parsed2.cleanPath) return null;
3647
+ return {
3648
+ path: parsed2.cleanPath,
3649
+ stat: parsed2.stat,
3650
+ added: parsed2.added,
3651
+ removed: parsed2.removed
3652
+ };
3653
+ }
3654
+ const parsed = parseTargetStatSuffix(trimmed);
3655
+ if (parsed.stat && parsed.cleanPath) {
3656
+ const candidatePath = parsed.cleanPath;
3657
+ if (!/^[+-]\d+$/.test(candidatePath) && !/^(?:Pending edit|Create file|File access|Question\b|Requesting permission\b)/i.test(candidatePath) && (candidatePath.includes("/") || candidatePath.startsWith("~") || candidatePath.startsWith(".") || /\.[a-z0-9]{1,10}$/i.test(candidatePath) || /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(candidatePath))) {
3658
+ return {
3659
+ path: candidatePath,
3660
+ stat: parsed.stat,
3661
+ added: parsed.added,
3662
+ removed: parsed.removed
3663
+ };
3664
+ }
3665
+ }
3666
+ return null;
3667
+ }
3668
+ function matchFileEditTargetLine(line) {
3669
+ return extractFileEditTargetAndStat(line)?.path ?? null;
3670
+ }
3671
+ function stitchWrappedPathAbove(lines, fromIndex, initialPath) {
3672
+ let stitched = initialPath;
3673
+ let topIndex = fromIndex;
3674
+ for (let prevIdx = fromIndex - 1; prevIdx >= Math.max(0, fromIndex - 3); prevIdx -= 1) {
3675
+ const prevRaw = lines[prevIdx];
3676
+ const prevTrimmed = prevRaw.trim();
3677
+ if (!prevTrimmed) break;
3678
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) break;
3679
+ if (/^(?:Pending edit|Create file|File access|Question\b|Requesting permission\b)/i.test(prevTrimmed)) break;
3680
+ if (parseInlineDiffLine(prevRaw) !== null || isFoldedDiffMarkerLine(prevTrimmed)) break;
3681
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(prevTrimmed) || /^[│\s]*[❯>]\s*$/.test(prevTrimmed)) break;
3682
+ const actionPrefix = prevTrimmed.match(/^(?:New file|Create file|Delete file|Write|Edit|Create):\s*(\S.*)$/i);
3683
+ if (actionPrefix) {
3684
+ stitched = `${actionPrefix[1].trim()}${stitched}`;
3685
+ topIndex = prevIdx;
3686
+ break;
3687
+ }
3688
+ const isValidPathSegment = !/\s/.test(prevTrimmed) && (prevTrimmed.includes("/") || prevTrimmed.startsWith("~") || prevTrimmed.startsWith(".") || stitched === "" && /\.[a-z0-9]{1,10}$/i.test(prevTrimmed));
3689
+ if (isValidPathSegment && (stitched === "" || prevTrimmed.length >= 40)) {
3690
+ stitched = `${prevTrimmed}${stitched}`;
3691
+ topIndex = prevIdx;
3692
+ if (stitched.startsWith("/") || stitched.startsWith("~/") || stitched.startsWith("./")) {
3693
+ break;
3694
+ }
3695
+ continue;
3696
+ }
3697
+ break;
3698
+ }
3699
+ return { path: stitched, topIndex };
3700
+ }
3701
+ function extractFileEditPrompt(snapshot) {
3702
+ const stripped = stripAnsiAndExpandRep(snapshot);
3703
+ const lines = stripped.trimEnd().split("\n");
3704
+ const lineInFence = computeFenceMask(lines);
3705
+ const windowStart = Math.max(0, lines.length - 30);
3706
+ let questionIndex = -1;
3707
+ for (let i = lines.length - 1; i >= windowStart; i -= 1) {
3708
+ if (lineInFence[i]) continue;
3709
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(lines[i]) && isStandaloneFileEditContext(lines, i, lineInFence)) {
3710
+ questionIndex = i;
3711
+ break;
3712
+ }
3713
+ }
3714
+ if (questionIndex < 0) return null;
3715
+ const questionText = lines[questionIndex].trim();
3716
+ let headingIndex = -1;
3717
+ let headingText = null;
3718
+ let targetIndex = -1;
3719
+ let targetPath = null;
3720
+ let headerStat;
3721
+ let headerAdded;
3722
+ let headerRemoved;
3723
+ let topBodyIndex = questionIndex;
3724
+ const itemsReversed = [];
3725
+ let currentHunkReversed = [];
3726
+ for (let index = questionIndex - 1; index >= 0; index -= 1) {
3727
+ const rawLine = lines[index];
3728
+ const trimmed = rawLine.trim();
3729
+ if (!trimmed) continue;
3730
+ if (/^(?:Pending edit|Create file)$/i.test(trimmed)) {
3731
+ headingIndex = index;
3732
+ headingText = trimmed;
3733
+ topBodyIndex = index + 1;
3734
+ break;
3735
+ }
3736
+ if (/^[─━═_-]{3,}$/.test(trimmed)) {
3737
+ topBodyIndex = index + 1;
3738
+ for (let prevIdx = index - 1; prevIdx >= Math.max(0, index - 4); prevIdx -= 1) {
3739
+ const prevTrimmed = lines[prevIdx].trim();
3740
+ if (!prevTrimmed) continue;
3741
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3742
+ headingIndex = prevIdx;
3743
+ headingText = prevTrimmed;
3744
+ }
3745
+ break;
3746
+ }
3747
+ break;
3748
+ }
3749
+ const diffLine = parseInlineDiffLine(rawLine);
3750
+ if (diffLine && targetIndex < 0) {
3751
+ currentHunkReversed.push(diffLine);
3752
+ topBodyIndex = index;
3753
+ continue;
3754
+ }
3755
+ if (isFoldedDiffMarkerLine(trimmed) && targetIndex < 0) {
3756
+ if (currentHunkReversed.length > 0) {
3757
+ itemsReversed.push({ kind: "hunk", lines: currentHunkReversed.reverse() });
3758
+ currentHunkReversed = [];
3759
+ }
3760
+ itemsReversed.push({ kind: "omitted", marker: trimmed });
3761
+ topBodyIndex = index;
3762
+ continue;
3763
+ }
3764
+ if (/^shift\+tab\s+to\s+auto-approve\s+file\s+(?:edits?|writes?)$/i.test(trimmed) || /^Reason:\s*\S/i.test(trimmed) || /^[⚠⚠️]\s*/.test(trimmed) || /^Esc disabled during file edits\b/i.test(trimmed) || /^>\s*Displayed as code block\.\s*Widen terminal to view inline\.$/i.test(trimmed) || /^\(no content\)$/i.test(trimmed)) {
3765
+ topBodyIndex = index;
3766
+ continue;
3767
+ }
3768
+ const extractedTarget = extractFileEditTargetAndStat(rawLine);
3769
+ let matchedTarget = extractedTarget?.path ?? null;
3770
+ let matchedStat = extractedTarget?.stat;
3771
+ let matchedAdded = extractedTarget?.added;
3772
+ let matchedRemoved = extractedTarget?.removed;
3773
+ let targetTopIdx = index;
3774
+ if (!matchedTarget && targetIndex < 0 && /^(?:\+\d+(?:\s+-\d+)?|-\d+)$/.test(trimmed) && index > 0) {
3775
+ const bareStat = parseTargetStatSuffix(`placeholder ${trimmed}`);
3776
+ const stitched = stitchWrappedPathAbove(lines, index, "");
3777
+ if (stitched.path) {
3778
+ matchedTarget = stitched.path;
3779
+ matchedStat = bareStat.stat;
3780
+ matchedAdded = bareStat.added;
3781
+ matchedRemoved = bareStat.removed;
3782
+ targetTopIdx = stitched.topIndex;
3783
+ }
3784
+ } else if (matchedTarget && targetIndex < 0 && !matchedTarget.startsWith("/") && !matchedTarget.startsWith("~/") && !matchedTarget.startsWith("./")) {
3785
+ const stitched = stitchWrappedPathAbove(lines, index, matchedTarget);
3786
+ matchedTarget = stitched.path;
3787
+ targetTopIdx = stitched.topIndex;
3788
+ }
3789
+ if (!matchedTarget && targetIndex < 0 && !/\s/.test(trimmed) && (trimmed.includes("/") || /\.[a-z0-9]{1,8}$/i.test(trimmed)) && !/^[─━═_-]{3,}$/.test(trimmed)) {
3790
+ for (let prevIdx = index - 1; prevIdx >= Math.max(0, index - 4); prevIdx -= 1) {
3791
+ const prevTrimmed = lines[prevIdx].trim();
3792
+ if (!prevTrimmed) continue;
3793
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) continue;
3794
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3795
+ matchedTarget = trimmed;
3796
+ }
3797
+ break;
3798
+ }
3799
+ }
3800
+ if (matchedTarget && targetIndex < 0) {
3801
+ targetIndex = targetTopIdx;
3802
+ targetPath = matchedTarget;
3803
+ headerStat = matchedStat;
3804
+ headerAdded = matchedAdded;
3805
+ headerRemoved = matchedRemoved;
3806
+ topBodyIndex = targetTopIdx;
3807
+ for (let prevIdx = targetTopIdx - 1; prevIdx >= Math.max(0, targetTopIdx - 6); prevIdx -= 1) {
3808
+ const prevTrimmed = lines[prevIdx].trim();
3809
+ if (!prevTrimmed) continue;
3810
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) {
3811
+ continue;
3812
+ }
3813
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3814
+ headingIndex = prevIdx;
3815
+ headingText = prevTrimmed;
3816
+ }
3817
+ break;
3818
+ }
3819
+ break;
3820
+ }
3821
+ break;
3822
+ }
3823
+ if (currentHunkReversed.length > 0) {
3824
+ itemsReversed.push({ kind: "hunk", lines: currentHunkReversed.reverse() });
3825
+ }
3826
+ const items = itemsReversed.reverse();
3827
+ const hunks = items.filter((item) => item.kind === "hunk").map((item) => item.lines);
3828
+ const omittedMarkers = items.filter((item) => item.kind === "omitted").map((item) => item.marker);
3829
+ const visibleAdded = hunks.reduce(
3830
+ (sum, hunk) => sum + hunk.filter((l) => l.type === "add").length,
3831
+ 0
3832
+ );
3833
+ const visibleRemoved = hunks.reduce(
3834
+ (sum, hunk) => sum + hunk.filter((l) => l.type === "remove").length,
3835
+ 0
3836
+ );
3837
+ const partialReasons = [];
3838
+ if (!targetPath && (hunks.length > 0 || omittedMarkers.length > 0)) {
3839
+ partialReasons.push("file header scrolled off-screen");
3840
+ }
3841
+ if (headerStat && (headerAdded !== void 0 && visibleAdded < headerAdded || headerRemoved !== void 0 && visibleRemoved < headerRemoved)) {
3842
+ partialReasons.push(
3843
+ `showing +${visibleAdded} -${visibleRemoved} of ${headerStat} total changes`
3844
+ );
3845
+ }
3846
+ const allOmissionNotes = [...partialReasons, ...omittedMarkers];
3847
+ const isPartial = allOmissionNotes.length > 0;
3848
+ const omissionNote = isPartial ? allOmissionNotes.join("; ") : void 0;
3849
+ const isCreate = /^Allow creation of this file\?$/i.test(questionText) || /^Create file$/i.test(headingText || "");
3850
+ const identity = targetPath || (isCreate ? "create file" : "file edit");
3851
+ const fullLine = targetPath ? `${questionText} (${targetPath})` : questionText;
3852
+ let body = "";
3853
+ if (hunks.length > 0 || omittedMarkers.length > 0) {
3854
+ const allAdditions = hunks.length > 0 && hunks.every((hunk) => hunk.every((l) => l.type === "add"));
3855
+ const diffOut = ["```diff"];
3856
+ if (targetPath) {
3857
+ const fromHeader = isCreate && allAdditions ? "--- /dev/null" : `--- a/${targetPath}`;
3858
+ const toHeader = `+++ b/${targetPath}`;
3859
+ diffOut.push(fromHeader, toHeader);
3860
+ }
3861
+ if (headerStat) {
3862
+ diffOut.push(`# diff-stat: ${headerStat}`);
3863
+ }
3864
+ for (const reason of partialReasons) {
3865
+ diffOut.push(`# partial-diff: ${reason}`);
3866
+ }
3867
+ for (const item of items) {
3868
+ if (item.kind === "omitted") {
3869
+ diffOut.push(`# omitted: ${item.marker}`);
3870
+ continue;
3871
+ }
3872
+ const hunk = item.lines;
3873
+ const oldCount = hunk.filter((l) => l.type === "remove" || l.type === "context").length;
3874
+ const newCount = hunk.filter((l) => l.type === "add" || l.type === "context").length;
3875
+ const firstOld = hunk.find((l) => l.type === "remove" || l.type === "context")?.lineNum ?? (oldCount === 0 ? 0 : 1);
3876
+ const firstNew = hunk.find((l) => l.type === "add" || l.type === "context")?.lineNum ?? (newCount === 0 ? 0 : 1);
3877
+ diffOut.push(`@@ -${firstOld},${oldCount} +${firstNew},${newCount} @@`);
3878
+ for (const l of hunk) {
3879
+ if (l.type === "remove") {
3880
+ diffOut.push(`-${l.content}`);
3881
+ } else if (l.type === "add") {
3882
+ diffOut.push(`+${l.content}`);
3883
+ } else {
3884
+ diffOut.push(` ${l.content}`);
3885
+ }
3886
+ }
3887
+ }
3888
+ diffOut.push("```");
3889
+ body = diffOut.join("\n");
3890
+ } else {
3891
+ const startIdx = headingIndex >= 0 ? headingIndex + 1 : topBodyIndex;
3892
+ const contextLines = lines.slice(startIdx, questionIndex).map((l) => l.trim()).filter((l) => {
3893
+ if (!l) return false;
3894
+ if (/^(?:Pending edit|Create file)$/i.test(l)) return false;
3895
+ if (/^[─━═_-]{3,}$/.test(l)) return false;
3896
+ if (/^shift\+tab\s+to\s+auto-approve\s+file\s+(?:edits?|writes?)$/i.test(l)) return false;
3897
+ return true;
3898
+ });
3899
+ body = contextLines.join("\n").trim();
3900
+ }
3901
+ return {
3902
+ identity,
3903
+ filePath: targetPath ?? void 0,
3904
+ fullLine,
3905
+ body,
3906
+ questionText,
3907
+ ...headerStat ? { diffStat: headerStat } : {},
3908
+ ...isPartial ? { partial: true, ...omissionNote ? { omissionNote } : {} } : {}
3909
+ };
3910
+ }
3565
3911
  function extractHeader(snapshot) {
3566
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3912
+ const stripped = stripAnsiAndExpandRep(snapshot);
3567
3913
  const matches = Array.from(stripped.matchAll(/Requesting permission for:\s*([^\n]+)/gi));
3568
3914
  if (matches.length === 0) return null;
3569
3915
  return matches[matches.length - 1][1].trim();
3570
3916
  }
3571
3917
  function extractFullHeaderLine(snapshot) {
3572
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3918
+ const stripped = stripAnsiAndExpandRep(snapshot);
3573
3919
  const matches = Array.from(stripped.matchAll(/Requesting permission for:\s*([^\n]+)/gi));
3574
3920
  if (matches.length === 0) return null;
3575
3921
  return matches[matches.length - 1][0].trim();
@@ -3605,7 +3951,7 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3605
3951
  for (let k = boundaryIdx - 1; k >= checkTop; k--) {
3606
3952
  const l = lines[k].trim();
3607
3953
  if (!l) continue;
3608
- if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /Question \d+\/\d+:/i.test(l)) {
3954
+ if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(l) || /Question \d+\/\d+:/i.test(l)) {
3609
3955
  const sliceBetween = lines.slice(k + 1, boundaryIdx);
3610
3956
  let optCount = 0;
3611
3957
  let hasFooter = false;
@@ -3613,6 +3959,10 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3613
3959
  for (const raw of sliceBetween) {
3614
3960
  const s = raw.trim();
3615
3961
  if (!s) continue;
3962
+ if (parseInlineDiffLine(raw) !== null || isFoldedDiffMarkerLine(raw)) {
3963
+ inDiff = true;
3964
+ continue;
3965
+ }
3616
3966
  if (inDiff) {
3617
3967
  if (/^[ +\-\\]/.test(raw)) {
3618
3968
  continue;
@@ -3624,7 +3974,7 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3624
3974
  inDiff = false;
3625
3975
  continue;
3626
3976
  }
3627
- if (/^Do you want to proceed\?/i.test(raw)) {
3977
+ if (/^(?:Do you want to proceed\?|Accept this file edit\?|Allow creation of this file\?)/i.test(raw)) {
3628
3978
  inDiff = false;
3629
3979
  continue;
3630
3980
  }
@@ -3656,7 +4006,7 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3656
4006
  if (/\benter\s+Select\b/i.test(l) || /\benter\s+to\s+(?:select|submit)\b/i.test(l)) {
3657
4007
  return false;
3658
4008
  }
3659
- if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l)) {
4009
+ if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l) || /^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(l)) {
3660
4010
  return false;
3661
4011
  }
3662
4012
  if (/\b(?:allow|deny)\s+access\b/i.test(l)) {
@@ -3675,7 +4025,7 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3675
4025
  for (let j = questionIdx - 1; j >= start; j--) {
3676
4026
  const l = lines[j].trim();
3677
4027
  if (!l) continue;
3678
- 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)) {
4028
+ 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) || parseInlineDiffLine(lines[j]) !== null || /Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(l) || /^(?:Pending edit|Create file)$/i.test(l) || /Question \d+\/\d+:/i.test(l)) {
3679
4029
  return false;
3680
4030
  }
3681
4031
  if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
@@ -3722,10 +4072,10 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3722
4072
  if (/Question \d+\/\d+:/i.test(l)) {
3723
4073
  return false;
3724
4074
  }
3725
- if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l)) {
4075
+ if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l) || /^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(l) || /^(?:Pending edit|Create file)$/i.test(l)) {
3726
4076
  return false;
3727
4077
  }
3728
- 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)) {
4078
+ 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) || parseInlineDiffLine(lines[j]) !== null) {
3729
4079
  return false;
3730
4080
  }
3731
4081
  if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l) || /^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
@@ -3737,11 +4087,100 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3737
4087
  }
3738
4088
  return true;
3739
4089
  }
4090
+ function isStandaloneFileEditContext(lines, questionIdx, fenceMask) {
4091
+ if (fenceMask && fenceMask[questionIdx]) {
4092
+ return false;
4093
+ }
4094
+ const windowEnd = Math.min(lines.length, questionIdx + 15);
4095
+ let optionCount = 0;
4096
+ let hasFileEditOption = false;
4097
+ let hasFooter = false;
4098
+ let promptEndIdx = questionIdx;
4099
+ for (let j = questionIdx + 1; j < windowEnd; j++) {
4100
+ if (fenceMask && fenceMask[j]) continue;
4101
+ const raw = lines[j];
4102
+ const l = raw.trim();
4103
+ if (!l) continue;
4104
+ if (/\benter\s+Select\b/i.test(l) || /\benter\s+to\s+(?:select|submit)\b/i.test(l)) {
4105
+ return false;
4106
+ }
4107
+ if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l) || /Allow access to this file\?/i.test(l)) {
4108
+ return false;
4109
+ }
4110
+ if (isAgyStatusFooterLine(l) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(l) || isComposerBoxAt(lines, j)) {
4111
+ if (/^esc to cancel\b/i.test(l)) {
4112
+ hasFooter = true;
4113
+ }
4114
+ break;
4115
+ }
4116
+ if (optionCount > 0 && isResumedTurnSignal(l)) {
4117
+ break;
4118
+ }
4119
+ if (/^[>\s❯]*[1-9]\.\s+\S/.test(l)) {
4120
+ optionCount++;
4121
+ promptEndIdx = j;
4122
+ if (/\b(?:accept|reject|allow|deny|create|change|file|yes|no)\b/i.test(l)) {
4123
+ hasFileEditOption = true;
4124
+ }
4125
+ continue;
4126
+ }
4127
+ if (/^(?:↑\/↓|↑|↓)\s*Navigate\b.*\b(?:tab\s+Amend|f\s+full\s+diff|ctrl\+r\s+Review)\b/i.test(l) || /^esc to cancel\b/i.test(l)) {
4128
+ hasFooter = true;
4129
+ promptEndIdx = j;
4130
+ continue;
4131
+ }
4132
+ if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
4133
+ break;
4134
+ }
4135
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l)) {
4136
+ break;
4137
+ }
4138
+ }
4139
+ if (optionCount < 2 || !hasFileEditOption || !hasFooter) {
4140
+ return false;
4141
+ }
4142
+ if (hasTrailingAgyComposer(lines, promptEndIdx, fenceMask)) {
4143
+ return false;
4144
+ }
4145
+ const checkTop = Math.max(0, questionIdx - 120);
4146
+ let hasStructuralEditAnchor = false;
4147
+ for (let j = questionIdx - 1; j >= checkTop; j--) {
4148
+ const raw = lines[j];
4149
+ const l = raw.trim();
4150
+ if (!l) continue;
4151
+ if (/^shift\+tab\s+to\s+auto-approve\s+file\s+(?:edits?|writes?)$/i.test(l) || /^>\s*Displayed as code block\.\s*Widen terminal to view inline\.$/i.test(l) || /^\(no content\)$/i.test(l)) {
4152
+ hasStructuralEditAnchor = true;
4153
+ continue;
4154
+ }
4155
+ if (/^(?:Pending edit|Create file)$/i.test(l)) {
4156
+ hasStructuralEditAnchor = true;
4157
+ break;
4158
+ }
4159
+ if (parseInlineDiffLine(raw) !== null || isFoldedDiffMarkerLine(l)) {
4160
+ hasStructuralEditAnchor = true;
4161
+ continue;
4162
+ }
4163
+ if (/^Reason:\s*\S/i.test(l) || /^[⚠⚠️]\s*/.test(l) || /^Esc disabled during file edits\b/i.test(l)) {
4164
+ continue;
4165
+ }
4166
+ const targetCandidate = matchFileEditTargetLine(raw);
4167
+ if (targetCandidate) {
4168
+ hasStructuralEditAnchor = true;
4169
+ break;
4170
+ }
4171
+ if (hasStructuralEditAnchor) {
4172
+ break;
4173
+ }
4174
+ return false;
4175
+ }
4176
+ return hasStructuralEditAnchor;
4177
+ }
3740
4178
  function detectActivePromptKind(snapshot) {
3741
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4179
+ const stripped = stripAnsiAndExpandRep(snapshot);
3742
4180
  const lines = stripped.trimEnd().split("\n");
3743
4181
  const windowStart = Math.max(0, lines.length - 30);
3744
4182
  const approvalRe = /Requesting permission for:/i;
4183
+ const fileEditQuestionRe = /^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i;
3745
4184
  const fileAccessRe = /^\s*File access\s*$/i;
3746
4185
  const fileAccessQuestionRe = /^\s*Allow access to this file\?\s*$/i;
3747
4186
  const lineInFence = computeFenceMask(lines);
@@ -3752,8 +4191,10 @@ function detectActivePromptKind(snapshot) {
3752
4191
  for (let i = lines.length - 1; i >= 0; i--) {
3753
4192
  const line = lines[i];
3754
4193
  if (i >= windowStart) {
3755
- if (approvalRe.test(line)) {
3756
- if (!lineInFence[i] && lastApprovalLineIdx < 0) {
4194
+ if (lastApprovalLineIdx < 0 && !lineInFence[i]) {
4195
+ if (approvalRe.test(line)) {
4196
+ lastApprovalLineIdx = i;
4197
+ } else if (fileEditQuestionRe.test(line) && isStandaloneFileEditContext(lines, i, lineInFence)) {
3757
4198
  lastApprovalLineIdx = i;
3758
4199
  }
3759
4200
  }
@@ -3828,7 +4269,9 @@ function detectActivePromptKind(snapshot) {
3828
4269
  }
3829
4270
  } else if (latestChooserIdx === lastApprovalLineIdx) {
3830
4271
  const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(remainingLines);
3831
- const hasApprovalFooter = /\besc to cancel\b/i.test(remainingLines);
4272
+ const hasApprovalFooter = /\besc to cancel\b|(?:↑\/↓|↑|↓)\s*Navigate\b.*\b(?:tab\s+Amend|f\s+full\s+diff|ctrl\+r\s+Review)\b/i.test(
4273
+ remainingLines
4274
+ );
3832
4275
  if (hasApprovalOptions && hasApprovalFooter) {
3833
4276
  return "approval";
3834
4277
  }
@@ -4134,7 +4577,7 @@ function isDurationToken(word) {
4134
4577
  return /^\d+[dhms](?:\d+[dhms])*$/i.test(word);
4135
4578
  }
4136
4579
  function extractSystemErrorPrompt(snapshot) {
4137
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4580
+ const stripped = stripAnsiAndExpandRep(snapshot);
4138
4581
  const lines = stripped.split("\n");
4139
4582
  let bannerIdx = -1;
4140
4583
  let rawMessage = "";
@@ -4240,7 +4683,7 @@ function isQuestionFooterLine(line) {
4240
4683
  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);
4241
4684
  }
4242
4685
  function extractQuestionHeader(snapshot) {
4243
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4686
+ const stripped = stripAnsiAndExpandRep(snapshot);
4244
4687
  const lines = stripped.split("\n");
4245
4688
  const lineInFence = computeFenceMask(lines);
4246
4689
  const prefixRe = /^\s*(?:[❯>]\s*)?(?:\?\s*(?:Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)?|Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)/i;
@@ -4257,7 +4700,7 @@ function extractQuestionHeader(snapshot) {
4257
4700
  return null;
4258
4701
  }
4259
4702
  function hashPromptSnapshot(snapshot) {
4260
- let normalized = snapshot.replace(ANSI_ESCAPE_REGEX, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
4703
+ let normalized = stripAnsiAndExpandRep(snapshot).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
4261
4704
  if (detectActivePromptKind(normalized) === "system_error") {
4262
4705
  normalized = normalized.replace(/\bResets\s+in\s+\d+[dhms](?:\s*\d+[dhms])*\.?/gi, "Resets in <duration>");
4263
4706
  }
@@ -4304,14 +4747,37 @@ function parseApprovalSnapshot(snapshot) {
4304
4747
  return parseSystemErrorSnapshot(stripped, snapshot, systemError);
4305
4748
  }
4306
4749
  }
4750
+ if (kind === "approval") {
4751
+ const lines = stripped.trimEnd().split("\n");
4752
+ const windowStart = Math.max(0, lines.length - 30);
4753
+ let lastCmdApprovalIdx = -1;
4754
+ let lastFileEditIdx = -1;
4755
+ for (let i = lines.length - 1; i >= windowStart; i -= 1) {
4756
+ const l = lines[i];
4757
+ if (lastCmdApprovalIdx < 0 && (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l))) {
4758
+ lastCmdApprovalIdx = i;
4759
+ }
4760
+ if (lastFileEditIdx < 0 && /^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(l)) {
4761
+ lastFileEditIdx = i;
4762
+ }
4763
+ }
4764
+ if (lastFileEditIdx > lastCmdApprovalIdx) {
4765
+ const fileEdit = extractFileEditPrompt(stripped);
4766
+ if (fileEdit) {
4767
+ return parseFileEditSnapshot(stripped, snapshot, fileEdit);
4768
+ }
4769
+ }
4770
+ }
4307
4771
  return parseApprovalUISnapshot(stripped, snapshot);
4308
4772
  }
4309
4773
  function buildApprovalSemanticKey(candidate) {
4310
4774
  const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
4775
+ const rawBody = (candidate.body ?? "").replace(/\r\n/g, "\n");
4776
+ const isDiffBody = /```diff\b/i.test(rawBody) || /^(?:Accept this file edit\?|Allow creation of this file\?)/i.test(candidate.fullHeaderLine ?? "");
4311
4777
  const payload = {
4312
4778
  kind: candidate.kind,
4313
4779
  headerText: normalize(candidate.headerText),
4314
- body: normalize(candidate.body),
4780
+ body: isDiffBody ? rawBody.trim() : normalize(rawBody),
4315
4781
  options: (candidate.options ?? []).map((option) => ({
4316
4782
  number: option.number,
4317
4783
  text: normalize(option.text)
@@ -4349,6 +4815,30 @@ function parseFileAccessSnapshot(stripped, originalSnapshot, prompt) {
4349
4815
  paneHash: hashPromptSnapshot(originalSnapshot)
4350
4816
  };
4351
4817
  }
4818
+ function parseFileEditSnapshot(stripped, originalSnapshot, prompt) {
4819
+ const lines = stripped.split("\n");
4820
+ let questionIndex = -1;
4821
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
4822
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(lines[index])) {
4823
+ questionIndex = index;
4824
+ break;
4825
+ }
4826
+ }
4827
+ const belowQuestion = questionIndex >= 0 ? lines.slice(questionIndex + 1).join("\n") : stripped;
4828
+ const options = parseOptions(belowQuestion);
4829
+ if (options.length < 2 || !hasNegativeApprovalOption(options)) return null;
4830
+ return {
4831
+ kind: "approval",
4832
+ headerText: prompt.identity,
4833
+ fullHeaderLine: prompt.fullLine,
4834
+ command: void 0,
4835
+ filePath: prompt.filePath,
4836
+ options,
4837
+ submitMap: buildApprovalSubmitMap(options),
4838
+ body: prompt.body,
4839
+ paneHash: hashPromptSnapshot(originalSnapshot)
4840
+ };
4841
+ }
4352
4842
  function parseSystemErrorSnapshot(stripped, originalSnapshot, prompt) {
4353
4843
  return {
4354
4844
  kind: "system_error",
@@ -4478,7 +4968,7 @@ function buildQuestionSubmitMap(options) {
4478
4968
  function isWriteInInputActive(paneText, expectedHeader, expectedStep, expectedBodyText) {
4479
4969
  if (!paneText) return false;
4480
4970
  if (detectActivePromptKind(paneText) !== "question") return false;
4481
- const stripped = paneText.replace(ANSI_ESCAPE_REGEX2, "");
4971
+ const stripped = stripAnsiAndExpandRep(paneText);
4482
4972
  const lines = stripped.trimEnd().split("\n");
4483
4973
  const windowStart = Math.max(0, lines.length - 30);
4484
4974
  let headerIdx = -1;
@@ -4611,12 +5101,13 @@ function extractIdentity(headerText) {
4611
5101
  function guessToolTypeFromIdentity(c) {
4612
5102
  if (c.kind === "question") return "ASK_QUESTION";
4613
5103
  if (c.command) return "RUN_COMMAND";
4614
- if (c.filePath) return "CODE_ACTION";
5104
+ if (c.filePath || c.headerText === "file edit" || c.headerText === "create file" || c.headerText === "file creation") {
5105
+ return "CODE_ACTION";
5106
+ }
4615
5107
  return "GENERIC";
4616
5108
  }
4617
- var ANSI_ESCAPE_REGEX2 = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
4618
5109
  function stripAnsi(s) {
4619
- return s.replace(ANSI_ESCAPE_REGEX2, "");
5110
+ return stripAnsiAndExpandRep(s);
4620
5111
  }
4621
5112
 
4622
5113
  // src/event-mapper.ts
@@ -4806,6 +5297,10 @@ function hookToolNameToTranscriptType(hookName) {
4806
5297
  switch (upper) {
4807
5298
  case "LIST_DIR":
4808
5299
  return "LIST_DIRECTORY";
5300
+ case "READ_FILE":
5301
+ return "VIEW_FILE";
5302
+ case "EDIT_FILE":
5303
+ case "WRITE_FILE":
4809
5304
  case "REPLACE_FILE_CONTENT":
4810
5305
  case "MULTI_REPLACE_FILE_CONTENT":
4811
5306
  case "WRITE_TO_FILE":
@@ -4842,6 +5337,8 @@ function extractFilePathFromArgs(toolName, args) {
4842
5337
  if (!args || typeof args !== "object") return void 0;
4843
5338
  const a = args;
4844
5339
  for (const key of [
5340
+ "Path",
5341
+ "AbsolutePath",
4845
5342
  "TargetFile",
4846
5343
  "FilePath",
4847
5344
  "file_path",
@@ -5478,7 +5975,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5478
5975
  toolType: matchingCall?.toolType ?? guessToolTypeFromIdentity(candidate),
5479
5976
  command: matchingCall?.command ?? candidate.command,
5480
5977
  filePath: matchingCall?.filePath ?? candidate.filePath,
5481
- rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown"),
5978
+ rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.headerText === "create file" || candidate.headerText === "file creation" ? "write_file" : candidate.headerText === "file edit" ? "edit_file" : candidate.command ?? candidate.filePath ?? "unknown"),
5482
5979
  questionText: synthLineMatch?.stitchedHeaderText ?? (candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText),
5483
5980
  // NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
5484
5981
  // that directly correspond to desktop keystrokes (submitMap). Transcript options
@@ -6800,8 +7297,11 @@ function friendlyHookToolName(rawName, resultType) {
6800
7297
  return "LS";
6801
7298
  case "GREP_SEARCH":
6802
7299
  return "Grep";
7300
+ case "READ_FILE":
6803
7301
  case "VIEW_FILE":
6804
7302
  return "Read";
7303
+ case "EDIT_FILE":
7304
+ case "WRITE_FILE":
6805
7305
  case "WRITE_TO_FILE":
6806
7306
  case "REPLACE_FILE_CONTENT":
6807
7307
  case "MULTI_REPLACE_FILE_CONTENT":
@@ -9494,14 +9994,19 @@ var McpServer = class _McpServer {
9494
9994
  });
9495
9995
  }
9496
9996
  const toolName = diffParsed ? "Edit" : state.pendingCall?.toolType;
9497
- const filePath = diffParsed?.filePath ?? state.pendingCall?.filePath;
9997
+ const filePath = state.pendingCall?.filePath ?? diffParsed?.filePath;
9498
9998
  const toolInput = diffParsed ? {
9499
- file_path: diffParsed.filePath,
9999
+ file_path: filePath,
9500
10000
  old_string: diffParsed.oldString,
9501
10001
  new_string: diffParsed.newString,
9502
10002
  old_start_line: diffParsed.oldStartLine,
9503
10003
  new_start_line: diffParsed.newStartLine,
9504
- diff: diffParsed.rawDiff
10004
+ diff: diffParsed.rawDiff,
10005
+ ...diffParsed.diffStat ? { diff_stat: diffParsed.diffStat } : {},
10006
+ ...diffParsed.partial ? {
10007
+ partial: true,
10008
+ ...diffParsed.omissionNote ? { omission_note: diffParsed.omissionNote } : {}
10009
+ } : {}
9505
10010
  } : void 0;
9506
10011
  const optionsForMobile = state.pendingCall ? {
9507
10012
  promptId: state.promptId,
@@ -9535,7 +10040,8 @@ var McpServer = class _McpServer {
9535
10040
  })()
9536
10041
  } : {}
9537
10042
  } : { promptId: state.promptId };
9538
- const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
10043
+ const baseContent = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
10044
+ const content = diffParsed?.partial && diffParsed.omissionNote && !baseContent.includes(diffParsed.omissionNote) ? `${baseContent} \u2014 partial diff (${diffParsed.omissionNote})` : baseContent;
9539
10045
  const input = {
9540
10046
  sessionId: session.sessionId,
9541
10047
  type: import_codevibe_core7.EventType.INTERACTIVE_PROMPT,
@@ -10451,6 +10957,9 @@ function parseFencedDiffFromBody(body, header) {
10451
10957
  const rawDiff = fenceMatch[1];
10452
10958
  const oldLines = [];
10453
10959
  const newLines = [];
10960
+ const omissionNotes = [];
10961
+ let diffStat;
10962
+ let partial = false;
10454
10963
  const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
10455
10964
  let currentOldLine = 0;
10456
10965
  let currentNewLine = 0;
@@ -10465,6 +10974,17 @@ function parseFencedDiffFromBody(body, header) {
10465
10974
  return p;
10466
10975
  };
10467
10976
  for (const line of rawDiff.split("\n")) {
10977
+ const statComment = line.match(/^#\s*diff-stat:\s*((?:\+\d+(?:\s+-\d+)?|-\d+))\s*$/i);
10978
+ if (statComment) {
10979
+ diffStat = statComment[1].replace(/\s+/g, " ").trim();
10980
+ continue;
10981
+ }
10982
+ const omissionComment = line.match(/^#\s*(?:partial-diff|omitted):\s*(.+)$/i);
10983
+ if (omissionComment) {
10984
+ partial = true;
10985
+ omissionNotes.push(omissionComment[1].trim());
10986
+ continue;
10987
+ }
10468
10988
  const hunkMatch = line.match(hunkHeaderPattern);
10469
10989
  if (hunkMatch) {
10470
10990
  hunkCount += 1;
@@ -10472,12 +10992,12 @@ function parseFencedDiffFromBody(body, header) {
10472
10992
  currentNewLine = Number.parseInt(hunkMatch[2], 10);
10473
10993
  continue;
10474
10994
  }
10475
- if (line.startsWith("+++ ")) {
10995
+ if (hunkCount === 0 && line.startsWith("+++ ")) {
10476
10996
  const pathVal = stripPrefix(line.slice(4));
10477
10997
  if (pathVal) filePath = pathVal;
10478
10998
  continue;
10479
10999
  }
10480
- if (line.startsWith("--- ")) {
11000
+ if (hunkCount === 0 && line.startsWith("--- ")) {
10481
11001
  if (!filePath) {
10482
11002
  const pathVal = stripPrefix(line.slice(4));
10483
11003
  if (pathVal) filePath = pathVal;
@@ -10504,8 +11024,9 @@ function parseFencedDiffFromBody(body, header) {
10504
11024
  oldStartLine = void 0;
10505
11025
  newStartLine = void 0;
10506
11026
  }
11027
+ const isFileEditHeader = /^(?:Accept this file edit\?|Allow creation of this file\?)/i.test(header ?? "");
10507
11028
  if (!filePath) {
10508
- const searchTargets = [body, header ?? ""];
11029
+ const searchTargets = isFileEditHeader ? [header ?? ""] : [body, header ?? ""];
10509
11030
  for (const target of searchTargets) {
10510
11031
  if (!target) continue;
10511
11032
  const m = target.match(/\[.*?\]\(file:\/\/([^\s\)]+)\)/) || target.match(/file:\/\/([^\s\)]+)/);
@@ -10525,7 +11046,12 @@ function parseFencedDiffFromBody(body, header) {
10525
11046
  newString: newLines.join("\n"),
10526
11047
  oldStartLine,
10527
11048
  newStartLine,
10528
- rawDiff
11049
+ rawDiff,
11050
+ ...diffStat ? { diffStat } : {},
11051
+ ...partial ? {
11052
+ partial: true,
11053
+ ...omissionNotes.length > 0 ? { omissionNote: omissionNotes.join("; ") } : {}
11054
+ } : {}
10529
11055
  };
10530
11056
  }
10531
11057
  var __testing = {
@@ -10550,6 +11076,7 @@ var __testing = {
10550
11076
  commandDigest,
10551
11077
  detectActivePromptKind,
10552
11078
  extractFileAccessPrompt,
11079
+ extractFileEditPrompt,
10553
11080
  extractSystemErrorPrompt,
10554
11081
  generateLaunchSessionId,
10555
11082
  getActiveConversationFromCliLog,
@@ -10564,5 +11091,6 @@ var __testing = {
10564
11091
  readTranscriptFirstType,
10565
11092
  reconstructFooter,
10566
11093
  runForeignSpawnIdle,
10567
- safeIsoTimestamp
11094
+ safeIsoTimestamp,
11095
+ stripAnsiAndExpandRep
10568
11096
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.100",
3
+ "version": "2.0.101",
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.88",
49
+ "@quantiya/codevibe-core": "^2.0.89",
50
50
  "chokidar": "^5.0.0",
51
51
  "dotenv": "^16.6.1",
52
52
  "express": "^5.1.0",