@quantiya/codevibe-antigravity-plugin 2.0.99 → 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",
@@ -4475,10 +4965,10 @@ function buildQuestionSubmitMap(options) {
4475
4965
  }
4476
4966
  return map;
4477
4967
  }
4478
- function isWriteInInputActive(paneText, expectedHeader, expectedStep) {
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;
@@ -4493,10 +4983,27 @@ function isWriteInInputActive(paneText, expectedHeader, expectedStep) {
4493
4983
  const extracted = extractQuestionHeader(rawHeader);
4494
4984
  if (!extracted) return false;
4495
4985
  if (expectedHeader) {
4986
+ const normHeaderBlock = (s) => s.replace(/^[❯>\s*•-]+\s*/, "").replace(/\s+/g, " ").trim().toLowerCase();
4496
4987
  const expectedExtracted = extractQuestionHeader(expectedHeader);
4497
- const expectedBody = (expectedExtracted ? expectedExtracted.body : expectedHeader).replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
4498
- const paneBody = extracted.body.replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
4499
- if (paneBody !== expectedBody) {
4988
+ const baseExpectedHeader = expectedExtracted ? expectedExtracted.body : expectedHeader;
4989
+ const expectedBodyLines = expectedBodyText ? expectedBodyText.split("\n").map((l) => l.trim()).filter(Boolean) : [];
4990
+ const allowedExpectedBodies = /* @__PURE__ */ new Set();
4991
+ for (let j = 0; j <= expectedBodyLines.length; j++) {
4992
+ allowedExpectedBodies.add(
4993
+ normHeaderBlock([baseExpectedHeader, ...expectedBodyLines.slice(0, j)].join(" "))
4994
+ );
4995
+ }
4996
+ const contLines = [];
4997
+ for (let j = headerIdx + 1; j < lines.length; j++) {
4998
+ const t = lines[j].trim();
4999
+ if (!t) continue;
5000
+ if (/^Your answer:\s*/i.test(t) || t.startsWith("(Enter to submit") || t.includes("Navigate") && t.includes("Select") || t.startsWith("\u2191/\u2193") || /^[❯>\s]*\d+\.\s+/.test(lines[j]) || /^[─━═-]{4,}$/.test(t)) {
5001
+ break;
5002
+ }
5003
+ contLines.push(t);
5004
+ }
5005
+ const paneBody = normHeaderBlock([extracted.body, ...contLines].join(" "));
5006
+ if (!allowedExpectedBodies.has(paneBody)) {
4500
5007
  return false;
4501
5008
  }
4502
5009
  }
@@ -4594,12 +5101,13 @@ function extractIdentity(headerText) {
4594
5101
  function guessToolTypeFromIdentity(c) {
4595
5102
  if (c.kind === "question") return "ASK_QUESTION";
4596
5103
  if (c.command) return "RUN_COMMAND";
4597
- 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
+ }
4598
5107
  return "GENERIC";
4599
5108
  }
4600
- var ANSI_ESCAPE_REGEX2 = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
4601
5109
  function stripAnsi(s) {
4602
- return s.replace(ANSI_ESCAPE_REGEX2, "");
5110
+ return stripAnsiAndExpandRep(s);
4603
5111
  }
4604
5112
 
4605
5113
  // src/event-mapper.ts
@@ -4789,6 +5297,10 @@ function hookToolNameToTranscriptType(hookName) {
4789
5297
  switch (upper) {
4790
5298
  case "LIST_DIR":
4791
5299
  return "LIST_DIRECTORY";
5300
+ case "READ_FILE":
5301
+ return "VIEW_FILE";
5302
+ case "EDIT_FILE":
5303
+ case "WRITE_FILE":
4792
5304
  case "REPLACE_FILE_CONTENT":
4793
5305
  case "MULTI_REPLACE_FILE_CONTENT":
4794
5306
  case "WRITE_TO_FILE":
@@ -4825,6 +5337,8 @@ function extractFilePathFromArgs(toolName, args) {
4825
5337
  if (!args || typeof args !== "object") return void 0;
4826
5338
  const a = args;
4827
5339
  for (const key of [
5340
+ "Path",
5341
+ "AbsolutePath",
4828
5342
  "TargetFile",
4829
5343
  "FilePath",
4830
5344
  "file_path",
@@ -5059,6 +5573,33 @@ function questionsMatch(hookText, paneText) {
5059
5573
  if (!h || !p) return false;
5060
5574
  return h === p;
5061
5575
  }
5576
+ function matchPaneQuestionLines(hookText, candidate) {
5577
+ const firstHeaderText = (candidate.headerText || candidate.fullHeaderLine || "").trim();
5578
+ const firstFullHeaderLine = (candidate.fullHeaderLine || candidate.headerText || "").trim();
5579
+ if (!hookText || !firstHeaderText) return null;
5580
+ const rawBodyLines = candidate.body ? candidate.body.split("\n") : [];
5581
+ for (let k = 0; k <= rawBodyLines.length; k++) {
5582
+ if (k > 0 && !rawBodyLines[k - 1].trim()) {
5583
+ break;
5584
+ }
5585
+ const consumedLines = rawBodyLines.slice(0, k).map((l) => l.trim()).filter(Boolean);
5586
+ const stitchedHeaderText = [firstHeaderText, ...consumedLines].join(" ").replace(/\s+/g, " ").trim();
5587
+ const stitchedFullHeaderLine = [firstFullHeaderLine, ...consumedLines].join(" ").replace(/\s+/g, " ").trim();
5588
+ if (questionsMatch(hookText, stitchedHeaderText) || questionsMatch(hookText, stitchedFullHeaderLine)) {
5589
+ const remainingBody = rawBodyLines.slice(k).join("\n").trim();
5590
+ return {
5591
+ matched: true,
5592
+ stitchedHeaderText,
5593
+ stitchedFullHeaderLine,
5594
+ remainingBody
5595
+ };
5596
+ }
5597
+ }
5598
+ return null;
5599
+ }
5600
+ function paneCandidateMatchesQuestion(hookText, candidate) {
5601
+ return matchPaneQuestionLines(hookText, candidate) !== null;
5602
+ }
5062
5603
  function correlatesQuestionInvocation(call, delivery) {
5063
5604
  if (!call) return false;
5064
5605
  const tn = (delivery.toolName ?? "").toLowerCase().trim();
@@ -5232,10 +5773,11 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5232
5773
  const scoreStep = (stepIdx) => {
5233
5774
  if (stepIdx < 0 || stepIdx >= sqs.length) return { score: -1, textMatch: false };
5234
5775
  const targetQ = sqs[stepIdx];
5235
- const textMatch = questionsMatch(targetQ.question, paneQuestionText);
5776
+ const textMatch = paneCandidateMatchesQuestion(targetQ.question, candidate);
5236
5777
  if (!textMatch) return { score: -1, textMatch: false };
5237
- const qOptTexts = (targetQ.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
5238
- const cOptTexts = (candidate.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
5778
+ const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
5779
+ const qOptTexts = (targetQ.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
5780
+ const cOptTexts = (candidate.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
5239
5781
  if (cOptTexts.length > 0 && qOptTexts.length > 0) {
5240
5782
  const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
5241
5783
  const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
@@ -5279,6 +5821,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5279
5821
  return null;
5280
5822
  }
5281
5823
  const targetStepQ = sqs[bestStepIdx];
5824
+ const stepLineMatch = matchPaneQuestionLines(targetStepQ?.question, candidate);
5282
5825
  if (targetStepQ?.isMultiSelect) {
5283
5826
  candidate.submitMap = {};
5284
5827
  }
@@ -5298,9 +5841,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5298
5841
  }
5299
5842
  existing.paneSemanticKey = candidateSemanticKey;
5300
5843
  existing.paneHash = candidate.paneHash;
5301
- if (candidate.headerText) existing.matchedPaneHeader = candidate.headerText;
5302
- if (candidate.fullHeaderLine) existing.paneDisplayHeader = candidate.fullHeaderLine;
5303
- if (candidate.body) existing.body = candidate.body;
5844
+ existing.matchedPaneHeader = stepLineMatch?.stitchedHeaderText ?? candidate.headerText ?? existing.matchedPaneHeader;
5845
+ existing.paneDisplayHeader = stepLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? existing.paneDisplayHeader;
5846
+ existing.body = stepLineMatch ? stepLineMatch.remainingBody || void 0 : candidate.body;
5304
5847
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5305
5848
  logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
5306
5849
  promptId: existing.promptId,
@@ -5329,7 +5872,16 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5329
5872
  if (!existing.paneSemanticKey) {
5330
5873
  existing.paneSemanticKey = candidateSemanticKey;
5331
5874
  }
5332
- if (candidate.body) existing.body = candidate.body;
5875
+ if (stepLineMatch) {
5876
+ existing.matchedPaneHeader = stepLineMatch.stitchedHeaderText;
5877
+ existing.paneDisplayHeader = stepLineMatch.stitchedFullHeaderLine;
5878
+ existing.body = stepLineMatch.remainingBody || void 0;
5879
+ if (existing.pendingCall && targetStepQ?.question) {
5880
+ existing.pendingCall.questionText = stepLineMatch.stitchedHeaderText;
5881
+ }
5882
+ } else if (candidate.body) {
5883
+ existing.body = candidate.body;
5884
+ }
5333
5885
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5334
5886
  return null;
5335
5887
  }
@@ -5352,7 +5904,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5352
5904
  for (const p of [...this.pendingPrompts.values()]) {
5353
5905
  if (p.conversationId === conversationId && p.kind === "question") {
5354
5906
  const hookQuestionText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
5355
- const isSameQuestion = questionsMatch(hookQuestionText, paneQuestionText);
5907
+ const lineMatch = matchPaneQuestionLines(hookQuestionText, candidate);
5908
+ const isSameQuestion = lineMatch !== null;
5356
5909
  if (isSameQuestion) {
5357
5910
  const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
5358
5911
  const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
@@ -5368,9 +5921,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5368
5921
  }
5369
5922
  p.paneSemanticKey = candidateSemanticKey;
5370
5923
  p.paneHash = candidate.paneHash;
5371
- if (candidate.headerText) p.matchedPaneHeader = candidate.headerText;
5372
- if (candidate.fullHeaderLine) p.paneDisplayHeader = candidate.fullHeaderLine;
5373
- if (candidate.body) p.body = candidate.body;
5924
+ p.matchedPaneHeader = lineMatch?.stitchedHeaderText ?? candidate.headerText ?? p.matchedPaneHeader;
5925
+ p.paneDisplayHeader = lineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? p.paneDisplayHeader;
5926
+ p.body = lineMatch ? lineMatch.remainingBody || void 0 : candidate.body;
5374
5927
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5375
5928
  logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
5376
5929
  promptId: p.promptId,
@@ -5382,6 +5935,14 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5382
5935
  if (!p.paneSemanticKey) {
5383
5936
  p.paneSemanticKey = candidateSemanticKey;
5384
5937
  }
5938
+ if (lineMatch) {
5939
+ p.matchedPaneHeader = lineMatch.stitchedHeaderText;
5940
+ p.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
5941
+ p.body = lineMatch.remainingBody || void 0;
5942
+ if (p.pendingCall) {
5943
+ p.pendingCall.questionText = lineMatch.stitchedHeaderText;
5944
+ }
5945
+ }
5385
5946
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5386
5947
  return null;
5387
5948
  }
@@ -5400,6 +5961,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5400
5961
  } else if (matchingCall) {
5401
5962
  matchingCall.matchedPromptId = promptId;
5402
5963
  }
5964
+ const matchedStepQuestionText = candidate.kind === "question" ? liveWizard?.call.structuredQuestions?.[liveWizard.activeStepIndex ?? 0]?.question ?? matchingCall?.structuredQuestions?.find((sq) => paneCandidateMatchesQuestion(sq.question, candidate))?.question ?? matchingCall?.questionText : void 0;
5965
+ const synthLineMatch = candidate.kind === "question" && matchedStepQuestionText ? matchPaneQuestionLines(matchedStepQuestionText, candidate) : null;
5403
5966
  const syntheticCall = {
5404
5967
  conversationId,
5405
5968
  intentStepIndex: matchingCall?.intentStepIndex ?? -1,
@@ -5412,8 +5975,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5412
5975
  toolType: matchingCall?.toolType ?? guessToolTypeFromIdentity(candidate),
5413
5976
  command: matchingCall?.command ?? candidate.command,
5414
5977
  filePath: matchingCall?.filePath ?? candidate.filePath,
5415
- rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown"),
5416
- questionText: candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText,
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"),
5979
+ questionText: synthLineMatch?.stitchedHeaderText ?? (candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText),
5417
5980
  // NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
5418
5981
  // that directly correspond to desktop keystrokes (submitMap). Transcript options
5419
5982
  // serve solely as fallback when pane options cannot be parsed.
@@ -5431,9 +5994,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5431
5994
  conversationId,
5432
5995
  pendingCall: syntheticCall,
5433
5996
  submitMap: candidate.submitMap,
5434
- matchedPaneHeader: candidate.headerText,
5435
- paneDisplayHeader: candidate.fullHeaderLine,
5436
- body: candidate.body,
5997
+ matchedPaneHeader: synthLineMatch?.stitchedHeaderText ?? candidate.headerText,
5998
+ paneDisplayHeader: synthLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine,
5999
+ body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
5437
6000
  paneOptions: candidate.options,
5438
6001
  paneSemanticKey: buildApprovalSemanticKey(candidate),
5439
6002
  kind: candidate.kind,
@@ -5471,8 +6034,19 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5471
6034
  if (p.conversationId === call.conversationId && p.kind === "question") {
5472
6035
  const existingText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
5473
6036
  const isSameInvocation = !p.pendingCall || p.pendingCall.intentStepIndex === -1 || call.intentStepIndex === -1 || p.pendingCall.intentStepIndex === call.intentStepIndex;
5474
- if (isSameInvocation && questionsMatch(existingText, questionText)) {
6037
+ const hookLineMatch = matchPaneQuestionLines(questionText, {
6038
+ headerText: p.matchedPaneHeader,
6039
+ fullHeaderLine: p.paneDisplayHeader,
6040
+ body: p.body
6041
+ });
6042
+ if (isSameInvocation && (questionsMatch(existingText, questionText) || hookLineMatch !== null)) {
6043
+ if (hookLineMatch) {
6044
+ p.matchedPaneHeader = hookLineMatch.stitchedHeaderText;
6045
+ p.paneDisplayHeader = hookLineMatch.stitchedFullHeaderLine;
6046
+ p.body = hookLineMatch.remainingBody || void 0;
6047
+ }
5475
6048
  if (p.pendingCall) {
6049
+ p.pendingCall.questionText = hookLineMatch?.stitchedHeaderText ?? call.questionText ?? p.pendingCall.questionText;
5476
6050
  p.pendingCall.structuredQuestions = call.structuredQuestions;
5477
6051
  if (call.intentStepIndex !== -1) {
5478
6052
  p.pendingCall.intentStepIndex = call.intentStepIndex;
@@ -5485,10 +6059,20 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5485
6059
  const isHookMultiSelect = Boolean(
5486
6060
  call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
5487
6061
  );
5488
- if (isHookMultiSelect && Object.keys(p.submitMap || {}).length > 0) {
6062
+ const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
6063
+ const hookRawOpts = call.structuredQuestions?.[0]?.options ?? call.questionOptions ?? [];
6064
+ const qOptTexts = hookRawOpts.map((o) => normOpt(typeof o === "string" ? o : o.text));
6065
+ const cOptTexts = (p.paneOptions || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
6066
+ const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
6067
+ const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
6068
+ const hasStructuredQuestion = Boolean(call.structuredQuestions && call.structuredQuestions.length > 0);
6069
+ const hasHookOptionMismatch = cOptTexts.length > 0 && (hasStructuredQuestion && qOptTexts.length === 0 || qOptTexts.length > 0 && (cEffectiveTexts.length !== qOptTexts.length || qOptTexts.some((q) => !cEffectiveTexts.includes(q)) || cEffectiveTexts.some((c) => !qOptTexts.includes(c))));
6070
+ if ((isHookMultiSelect || hasHookOptionMismatch) && Object.keys(p.submitMap || {}).length > 0) {
5489
6071
  p.submitMap = {};
5490
- logger.info("emitHookQuestionPrompt: revoked actionable submitMap because incoming hook is multi-select", {
5491
- promptId: p.promptId
6072
+ logger.info("emitHookQuestionPrompt: revoked actionable submitMap due to multi-select or option mismatch", {
6073
+ promptId: p.promptId,
6074
+ isHookMultiSelect,
6075
+ hasHookOptionMismatch
5492
6076
  });
5493
6077
  this.emit("prompt-upgraded", p);
5494
6078
  }
@@ -5892,7 +6476,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5892
6476
  if (candidate.kind === "question") {
5893
6477
  const questionCalls = Array.from(this.pendingCalls.values()).filter((c) => c.conversationId === conversationId).filter((c) => c.resultStepIndex === void 0).filter((c) => {
5894
6478
  if (c.structuredQuestions && candidate.headerText) {
5895
- const matchesStructured = c.structuredQuestions.some((sq) => questionsMatch(sq.question, candidate.headerText));
6479
+ const matchesStructured = c.structuredQuestions.some((sq) => paneCandidateMatchesQuestion(sq.question, candidate));
5896
6480
  if (matchesStructured) return true;
5897
6481
  }
5898
6482
  const associatedPromptId = c.matchedPromptId || c.promptId;
@@ -5904,9 +6488,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5904
6488
  if (questionCalls.length > 0 && candidate.headerText) {
5905
6489
  const textMatch = questionCalls.find((c) => {
5906
6490
  if (!c.questionText) return false;
5907
- if (questionsMatch(c.questionText, candidate.headerText)) return true;
6491
+ if (paneCandidateMatchesQuestion(c.questionText, candidate)) return true;
5908
6492
  if (c.structuredQuestions) {
5909
- return c.structuredQuestions.some((sq) => questionsMatch(sq.question, candidate.headerText));
6493
+ return c.structuredQuestions.some((sq) => paneCandidateMatchesQuestion(sq.question, candidate));
5910
6494
  }
5911
6495
  return false;
5912
6496
  });
@@ -6139,7 +6723,7 @@ var PromptResponder = class {
6139
6723
  deliveryPhase
6140
6724
  };
6141
6725
  }
6142
- if (isQuestionPrompt && isWriteInInputActive(snapshot, state.matchedPaneHeader, expectedStep)) {
6726
+ if (isQuestionPrompt && (isWriteInInputActive(snapshot, state.matchedPaneHeader, expectedStep, state.body) || isWriteInInputActive(snapshot, void 0, expectedStep))) {
6143
6727
  return {
6144
6728
  ok: false,
6145
6729
  reason: "write-in-already-active",
@@ -6199,7 +6783,7 @@ var PromptResponder = class {
6199
6783
  await delay(100);
6200
6784
  try {
6201
6785
  const liveSnap = await this.paneObserver.captureSnapshot();
6202
- if (liveSnap && isWriteInInputActive(liveSnap, state.matchedPaneHeader, expectedStep)) {
6786
+ if (liveSnap && isWriteInInputActive(liveSnap, state.matchedPaneHeader, expectedStep, state.body)) {
6203
6787
  writeInReady = true;
6204
6788
  break;
6205
6789
  }
@@ -6237,7 +6821,7 @@ var PromptResponder = class {
6237
6821
  if (this.paneObserver) {
6238
6822
  try {
6239
6823
  const postHookSnap = await this.paneObserver.captureSnapshot();
6240
- if (!postHookSnap || !isWriteInInputActive(postHookSnap, state.matchedPaneHeader, expectedStep)) {
6824
+ if (!postHookSnap || !isWriteInInputActive(postHookSnap, state.matchedPaneHeader, expectedStep, state.body)) {
6241
6825
  return {
6242
6826
  ok: false,
6243
6827
  reason: "prompt-probe-failed",
@@ -6266,7 +6850,7 @@ var PromptResponder = class {
6266
6850
  if (this.paneObserver) {
6267
6851
  try {
6268
6852
  const preEnterSnap = await this.paneObserver.captureSnapshot();
6269
- if (!preEnterSnap || !isWriteInInputActive(preEnterSnap, state.matchedPaneHeader, expectedStep)) {
6853
+ if (!preEnterSnap || !isWriteInInputActive(preEnterSnap, state.matchedPaneHeader, expectedStep, state.body)) {
6270
6854
  return {
6271
6855
  ok: false,
6272
6856
  reason: "prompt-probe-failed",
@@ -6713,8 +7297,11 @@ function friendlyHookToolName(rawName, resultType) {
6713
7297
  return "LS";
6714
7298
  case "GREP_SEARCH":
6715
7299
  return "Grep";
7300
+ case "READ_FILE":
6716
7301
  case "VIEW_FILE":
6717
7302
  return "Read";
7303
+ case "EDIT_FILE":
7304
+ case "WRITE_FILE":
6718
7305
  case "WRITE_TO_FILE":
6719
7306
  case "REPLACE_FILE_CONTENT":
6720
7307
  case "MULTI_REPLACE_FILE_CONTENT":
@@ -9407,14 +9994,19 @@ var McpServer = class _McpServer {
9407
9994
  });
9408
9995
  }
9409
9996
  const toolName = diffParsed ? "Edit" : state.pendingCall?.toolType;
9410
- const filePath = diffParsed?.filePath ?? state.pendingCall?.filePath;
9997
+ const filePath = state.pendingCall?.filePath ?? diffParsed?.filePath;
9411
9998
  const toolInput = diffParsed ? {
9412
- file_path: diffParsed.filePath,
9999
+ file_path: filePath,
9413
10000
  old_string: diffParsed.oldString,
9414
10001
  new_string: diffParsed.newString,
9415
10002
  old_start_line: diffParsed.oldStartLine,
9416
10003
  new_start_line: diffParsed.newStartLine,
9417
- 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
+ } : {}
9418
10010
  } : void 0;
9419
10011
  const optionsForMobile = state.pendingCall ? {
9420
10012
  promptId: state.promptId,
@@ -9448,7 +10040,8 @@ var McpServer = class _McpServer {
9448
10040
  })()
9449
10041
  } : {}
9450
10042
  } : { promptId: state.promptId };
9451
- 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;
9452
10045
  const input = {
9453
10046
  sessionId: session.sessionId,
9454
10047
  type: import_codevibe_core7.EventType.INTERACTIVE_PROMPT,
@@ -10364,6 +10957,9 @@ function parseFencedDiffFromBody(body, header) {
10364
10957
  const rawDiff = fenceMatch[1];
10365
10958
  const oldLines = [];
10366
10959
  const newLines = [];
10960
+ const omissionNotes = [];
10961
+ let diffStat;
10962
+ let partial = false;
10367
10963
  const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
10368
10964
  let currentOldLine = 0;
10369
10965
  let currentNewLine = 0;
@@ -10378,6 +10974,17 @@ function parseFencedDiffFromBody(body, header) {
10378
10974
  return p;
10379
10975
  };
10380
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
+ }
10381
10988
  const hunkMatch = line.match(hunkHeaderPattern);
10382
10989
  if (hunkMatch) {
10383
10990
  hunkCount += 1;
@@ -10385,12 +10992,12 @@ function parseFencedDiffFromBody(body, header) {
10385
10992
  currentNewLine = Number.parseInt(hunkMatch[2], 10);
10386
10993
  continue;
10387
10994
  }
10388
- if (line.startsWith("+++ ")) {
10995
+ if (hunkCount === 0 && line.startsWith("+++ ")) {
10389
10996
  const pathVal = stripPrefix(line.slice(4));
10390
10997
  if (pathVal) filePath = pathVal;
10391
10998
  continue;
10392
10999
  }
10393
- if (line.startsWith("--- ")) {
11000
+ if (hunkCount === 0 && line.startsWith("--- ")) {
10394
11001
  if (!filePath) {
10395
11002
  const pathVal = stripPrefix(line.slice(4));
10396
11003
  if (pathVal) filePath = pathVal;
@@ -10417,8 +11024,9 @@ function parseFencedDiffFromBody(body, header) {
10417
11024
  oldStartLine = void 0;
10418
11025
  newStartLine = void 0;
10419
11026
  }
11027
+ const isFileEditHeader = /^(?:Accept this file edit\?|Allow creation of this file\?)/i.test(header ?? "");
10420
11028
  if (!filePath) {
10421
- const searchTargets = [body, header ?? ""];
11029
+ const searchTargets = isFileEditHeader ? [header ?? ""] : [body, header ?? ""];
10422
11030
  for (const target of searchTargets) {
10423
11031
  if (!target) continue;
10424
11032
  const m = target.match(/\[.*?\]\(file:\/\/([^\s\)]+)\)/) || target.match(/file:\/\/([^\s\)]+)/);
@@ -10438,7 +11046,12 @@ function parseFencedDiffFromBody(body, header) {
10438
11046
  newString: newLines.join("\n"),
10439
11047
  oldStartLine,
10440
11048
  newStartLine,
10441
- rawDiff
11049
+ rawDiff,
11050
+ ...diffStat ? { diffStat } : {},
11051
+ ...partial ? {
11052
+ partial: true,
11053
+ ...omissionNotes.length > 0 ? { omissionNote: omissionNotes.join("; ") } : {}
11054
+ } : {}
10442
11055
  };
10443
11056
  }
10444
11057
  var __testing = {
@@ -10463,6 +11076,7 @@ var __testing = {
10463
11076
  commandDigest,
10464
11077
  detectActivePromptKind,
10465
11078
  extractFileAccessPrompt,
11079
+ extractFileEditPrompt,
10466
11080
  extractSystemErrorPrompt,
10467
11081
  generateLaunchSessionId,
10468
11082
  getActiveConversationFromCliLog,
@@ -10477,5 +11091,6 @@ var __testing = {
10477
11091
  readTranscriptFirstType,
10478
11092
  reconstructFooter,
10479
11093
  runForeignSpawnIdle,
10480
- safeIsoTimestamp
11094
+ safeIsoTimestamp,
11095
+ stripAnsiAndExpandRep
10481
11096
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.99",
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",