@quantiya/codevibe-antigravity-plugin 2.0.100 → 2.0.102

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
  }
@@ -2439,6 +2448,10 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2439
2448
  * start()/stop() lifecycle and doesn't distinguish debounce
2440
2449
  * cycles. (Stage 2 iter-4 R1 HIGH finding 2026-05-21.) */
2441
2450
  this.debounceCycleId = 0;
2451
+ this.clearDebounceTimer = null;
2452
+ this.clearDebounceWindowOpenedAt = null;
2453
+ this.clearDebounceCycleId = 0;
2454
+ this.clearFlushInFlight = false;
2442
2455
  this.pipeFilePath = options.pipeFilePath ?? path6.join(os5.tmpdir(), `codevibe-agy-pane-${process.pid}.log`);
2443
2456
  this.skipTmuxCommands = options.skipTmuxCommands ?? false;
2444
2457
  this.captureFn = options.captureFn ?? null;
@@ -2473,6 +2486,9 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2473
2486
  * and they paint atomically. */
2474
2487
  this.QUESTION_SETTLE_DELAY_MS = 100;
2475
2488
  }
2489
+ static {
2490
+ this.CLEAR_CONFIRM_DELAY_MS = 50;
2491
+ }
2476
2492
  /**
2477
2493
  * Start observing the tmux pane targeted by `sessionName`. If sessionName
2478
2494
  * is omitted, read $CODEVIBE_AGY_TMUX_TARGET from env (set by the
@@ -2638,7 +2654,8 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2638
2654
  const err = extractSystemErrorPrompt(snapshot);
2639
2655
  headerText = err ? err.message : null;
2640
2656
  } else if (kind === "approval") {
2641
- headerText = extractHeader(snapshot);
2657
+ const fileEdit = extractFileEditPrompt(snapshot);
2658
+ headerText = fileEdit ? fileEdit.identity : extractHeader(snapshot);
2642
2659
  }
2643
2660
  }
2644
2661
  let isChooser = hasPrompt && kind !== "system_error";
@@ -2697,6 +2714,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2697
2714
  * goes idle (e.g. an animated spinner inside the approval UI).
2698
2715
  */
2699
2716
  scheduleDebouncedEmit(candidate) {
2717
+ this.cancelClearDebounce();
2700
2718
  const now = Date.now();
2701
2719
  this.pendingEmitCandidate = candidate;
2702
2720
  if (this.debounceWindowOpenedAt === null) {
@@ -2744,7 +2762,10 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2744
2762
  let candidate = staged;
2745
2763
  try {
2746
2764
  const freshSnapshot = await this.captureSnapshot();
2765
+ if (this.generation !== scheduledGeneration || !this.started) return;
2766
+ if (this.debounceCycleId !== scheduledCycleId) return;
2747
2767
  if (freshSnapshot && looksLikePromptSnapshot(freshSnapshot)) {
2768
+ this.cancelClearDebounce();
2748
2769
  candidate = {
2749
2770
  rawDelta: staged.rawDelta,
2750
2771
  snapshot: freshSnapshot,
@@ -2764,6 +2785,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2764
2785
  const settledHeader = settled ? extractQuestionHeader(settled) : null;
2765
2786
  const sameBody = !!firstHeader && !!settledHeader && firstHeader.body === settledHeader.body;
2766
2787
  if (settled && looksLikePromptSnapshot(settled) && detectActivePromptKind(settled) === "question" && sameBody) {
2788
+ this.cancelClearDebounce();
2767
2789
  candidate = {
2768
2790
  rawDelta: staged.rawDelta,
2769
2791
  snapshot: settled,
@@ -2781,7 +2803,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2781
2803
  if (candidate.snapshotHash === this.lastPromptHash) return;
2782
2804
  this.lastPromptHash = candidate.snapshotHash;
2783
2805
  this.approvalUIActive = true;
2784
- this.currentApprovalHeader = extractHeader(candidate.snapshot);
2806
+ this.currentApprovalHeader = extractFileEditPrompt(candidate.snapshot)?.identity ?? extractHeader(candidate.snapshot);
2785
2807
  this.emit("prompt-candidate", candidate);
2786
2808
  }
2787
2809
  /**
@@ -2815,6 +2837,64 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2815
2837
  this.pendingEmitCandidate = null;
2816
2838
  this.debounceWindowOpenedAt = null;
2817
2839
  this.debounceCycleId += 1;
2840
+ this.cancelClearDebounce();
2841
+ }
2842
+ cancelClearDebounce() {
2843
+ if (this.clearDebounceTimer) {
2844
+ clearTimeout(this.clearDebounceTimer);
2845
+ this.clearDebounceTimer = null;
2846
+ }
2847
+ this.clearDebounceWindowOpenedAt = null;
2848
+ this.clearFlushInFlight = false;
2849
+ this.clearDebounceCycleId += 1;
2850
+ }
2851
+ /**
2852
+ * Schedule a debounced check for whether the active prompt UI has genuinely
2853
+ * cleared from the pane. Ink full-viewport redraws write upper scrollback
2854
+ * chunks (`!looksLikePromptDelta(chunk)`) before repainting the bottom
2855
+ * chooser; capturing immediately mid-redraw would false-fire
2856
+ * `prompt-cleared` and cause the repainted chooser to re-emit a duplicate card.
2857
+ * Bounded by DEBOUNCE_MAX_WAIT_MS (600ms) so continuous post-approval output
2858
+ * never starves `prompt-cleared` (Claude R1-F2, Codex R1-F3).
2859
+ */
2860
+ scheduleDebouncedClear() {
2861
+ if (!this.approvalUIActive || this.clearFlushInFlight) return;
2862
+ const now = Date.now();
2863
+ if (this.clearDebounceWindowOpenedAt === null) {
2864
+ this.clearDebounceWindowOpenedAt = now;
2865
+ }
2866
+ const elapsed = now - this.clearDebounceWindowOpenedAt;
2867
+ const cappedRemaining = Math.max(0, _TmuxPaneObserver.DEBOUNCE_MAX_WAIT_MS - elapsed);
2868
+ if (cappedRemaining === 0 && this.clearDebounceTimer !== null) {
2869
+ return;
2870
+ }
2871
+ const waitMs = Math.min(_TmuxPaneObserver.DEBOUNCE_QUIET_MS, cappedRemaining);
2872
+ if (this.clearDebounceTimer) clearTimeout(this.clearDebounceTimer);
2873
+ this.clearDebounceCycleId += 1;
2874
+ const myGeneration = this.generation;
2875
+ const myCycleId = this.clearDebounceCycleId;
2876
+ this.clearDebounceTimer = setTimeout(() => {
2877
+ void this.flushDebouncedClear(myGeneration, myCycleId);
2878
+ }, waitMs);
2879
+ if (this.clearDebounceTimer.unref) this.clearDebounceTimer.unref();
2880
+ }
2881
+ async flushDebouncedClear(scheduledGeneration, scheduledCycleId) {
2882
+ if (this.generation !== scheduledGeneration || !this.started) return;
2883
+ if (this.clearDebounceCycleId !== scheduledCycleId) return;
2884
+ this.clearDebounceTimer = null;
2885
+ if (!this.approvalUIActive) {
2886
+ this.clearDebounceWindowOpenedAt = null;
2887
+ return;
2888
+ }
2889
+ this.clearFlushInFlight = true;
2890
+ try {
2891
+ await this.checkPromptCleared(scheduledGeneration, scheduledCycleId);
2892
+ } finally {
2893
+ if (this.clearDebounceCycleId === scheduledCycleId) {
2894
+ this.clearFlushInFlight = false;
2895
+ this.clearDebounceWindowOpenedAt = null;
2896
+ }
2897
+ }
2818
2898
  }
2819
2899
  /**
2820
2900
  * Public hook for tests / drivers: trigger a read+process cycle as if
@@ -2836,6 +2916,11 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2836
2916
  this.debounceTimer = null;
2837
2917
  await this.flushDebouncedEmit(this.generation, this.debounceCycleId);
2838
2918
  }
2919
+ if (this.clearDebounceTimer) {
2920
+ clearTimeout(this.clearDebounceTimer);
2921
+ this.clearDebounceTimer = null;
2922
+ await this.flushDebouncedClear(this.generation, this.clearDebounceCycleId);
2923
+ }
2839
2924
  }
2840
2925
  async processFileChanges() {
2841
2926
  if (this.processing) {
@@ -2853,7 +2938,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2853
2938
  if (!chunk) continue;
2854
2939
  if (!looksLikePromptDelta(chunk)) {
2855
2940
  if (this.approvalUIActive) {
2856
- await this.checkPromptCleared(myGeneration);
2941
+ this.scheduleDebouncedClear();
2857
2942
  }
2858
2943
  continue;
2859
2944
  }
@@ -2861,10 +2946,11 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2861
2946
  if (this.generation !== myGeneration || !this.started) return;
2862
2947
  if (!snapshot || !looksLikePromptSnapshot(snapshot)) {
2863
2948
  if (this.approvalUIActive) {
2864
- await this.checkPromptCleared(myGeneration);
2949
+ this.scheduleDebouncedClear();
2865
2950
  }
2866
2951
  continue;
2867
2952
  }
2953
+ this.cancelClearDebounce();
2868
2954
  const snapshotHash = hashPromptSnapshot(snapshot);
2869
2955
  if (snapshotHash === this.lastPromptHash) {
2870
2956
  continue;
@@ -2888,13 +2974,31 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2888
2974
  }
2889
2975
  }
2890
2976
  /** Called when we suspect the approval UI may have vanished. Does a
2891
- * fresh capture; if the pane no longer shows approval UI, fires
2892
- * 'prompt-cleared'. Generation-checked to suppress emit after stop(). */
2893
- async checkPromptCleared(myGeneration) {
2977
+ * fresh capture; if the pane no longer shows approval UI across two
2978
+ * consecutive captures, fires 'prompt-cleared'. Generation-checked
2979
+ * and cycle-checked to suppress emit after stop() or concurrent prompt
2980
+ * re-assertion (Claude R1-F3, Codex R1-F4). */
2981
+ async checkPromptCleared(myGeneration, expectedClearCycleId) {
2894
2982
  try {
2895
2983
  const snapshot = await this.captureSnapshot();
2896
2984
  if (this.generation !== myGeneration || !this.started) return;
2897
- if (!looksLikePromptSnapshot(snapshot)) {
2985
+ if (expectedClearCycleId !== void 0 && this.clearDebounceCycleId !== expectedClearCycleId) {
2986
+ return;
2987
+ }
2988
+ if (looksLikePromptSnapshot(snapshot)) {
2989
+ return;
2990
+ }
2991
+ await sleep(_TmuxPaneObserver.CLEAR_CONFIRM_DELAY_MS);
2992
+ if (this.generation !== myGeneration || !this.started) return;
2993
+ if (expectedClearCycleId !== void 0 && this.clearDebounceCycleId !== expectedClearCycleId) {
2994
+ return;
2995
+ }
2996
+ const confirmSnapshot = await this.captureSnapshot();
2997
+ if (this.generation !== myGeneration || !this.started) return;
2998
+ if (expectedClearCycleId !== void 0 && this.clearDebounceCycleId !== expectedClearCycleId) {
2999
+ return;
3000
+ }
3001
+ if (!looksLikePromptSnapshot(confirmSnapshot)) {
2898
3002
  this.approvalUIActive = false;
2899
3003
  this.lastPromptHash = null;
2900
3004
  this.currentApprovalHeader = null;
@@ -2944,6 +3048,19 @@ function isComposerBoxAt(lines, idx) {
2944
3048
  return false;
2945
3049
  }
2946
3050
  var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
3051
+ var ANSI_NON_REP_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-ac-~])/g;
3052
+ function stripAnsiAndExpandRep(input) {
3053
+ if (!input.includes("\x1B")) return input;
3054
+ const withoutNonRep = input.replace(ANSI_NON_REP_ESCAPE_REGEX, "");
3055
+ const expanded = withoutNonRep.replace(
3056
+ /([^\x00-\x1f\x7f])\x1B\[(\d*)b/gu,
3057
+ (_, ch, countStr) => {
3058
+ const count = Math.min(Math.max(Number.parseInt(countStr || "1", 10) || 1, 1), 512);
3059
+ return ch.repeat(1 + count);
3060
+ }
3061
+ );
3062
+ return expanded.replace(ANSI_ESCAPE_REGEX, "");
3063
+ }
2947
3064
  function isPaneInputSafetyBlocked(input) {
2948
3065
  const rawText = Array.isArray(input) ? input.join("\n") : input;
2949
3066
  if (!rawText) return { blocked: false, kind: null };
@@ -2962,7 +3079,7 @@ function isPaneInputSafetyBlocked(input) {
2962
3079
  if (isQuestionFooterLine(trimmed)) {
2963
3080
  footerIdx = i;
2964
3081
  break;
2965
- } else if (/\besc to cancel\b/i.test(trimmed) && !isAgyStatusFooterLine(trimmed)) {
3082
+ } 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
3083
  footerIdx = i;
2967
3084
  break;
2968
3085
  }
@@ -3048,7 +3165,7 @@ function isPaneInputSafetyBlocked(input) {
3048
3165
  candidateHeaders.push({ lineIdx: i, kind: "question" });
3049
3166
  } else if (/Allow access to this file\?/i.test(trimmed)) {
3050
3167
  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)) {
3168
+ } 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
3169
  candidateHeaders.push({ lineIdx: i, kind: "approval" });
3053
3170
  }
3054
3171
  }
@@ -3116,7 +3233,7 @@ function looksLikePromptDelta(chunk) {
3116
3233
  const text = chunk.includes("\x1B") ? chunk.replace(ANSI_ESCAPE_REGEX, "") : chunk;
3117
3234
  return (
3118
3235
  // 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)
3236
+ /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
3237
  );
3121
3238
  }
3122
3239
  function isNonSystemErrorWarningLine(trimmed) {
@@ -3152,7 +3269,7 @@ function isSystemErrorBannerLineInContext(lines, idx) {
3152
3269
  if (/^[─━│╭╰╮╯\s_-]*$/.test(nextTrimmed)) break;
3153
3270
  if (/^[│\s]*[❯>]/.test(nextTrimmed)) break;
3154
3271
  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;
3272
+ if (/Requesting permission for:|Question \d+\/\d+:|^\s*(?:File access|Pending edit|Create file)\s*$|^[>\s]*[1-9]\. \S/i.test(nextTrimmed)) break;
3156
3273
  const idMatch = nextTrimmed.match(/^Error ID:\s*([0-9a-fA-F-]*)$/i);
3157
3274
  if (idMatch) {
3158
3275
  if (/[0-9a-fA-F]/.test(idMatch[1])) return true;
@@ -3272,7 +3389,7 @@ function hasTrailingAgyComposer(lines, afterLineIdx, fenceMask) {
3272
3389
  );
3273
3390
  if (hasStatusLine) {
3274
3391
  const nonDecorativeNonStatus = trailingLines.filter(
3275
- (l) => !isAgyStatusFooterLine(l) && !isQuestionFooterLine(l) && !/^[─━│╭╰╮╯\s_-]{3,}$/.test(l)
3392
+ (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
3393
  );
3277
3394
  if (nonDecorativeNonStatus.length > 0) {
3278
3395
  return true;
@@ -3344,9 +3461,10 @@ function isSystemErrorActiveFromIndex(lines, errorIdx) {
3344
3461
  if (/^●\s*(?:[A-Za-z_-]+)\(/.test(trimmed)) return false;
3345
3462
  if (/Requesting permission for:/i.test(trimmed)) return false;
3346
3463
  if (/Question \d+\/\d+:/i.test(trimmed)) return false;
3347
- if (/^\s*File access\s*$/i.test(trimmed)) return false;
3464
+ if (/^\s*(?:File access|Pending edit|Create file)\s*$/i.test(trimmed)) return false;
3348
3465
  if (/^[>\s]*[1-9]\. \S/.test(trimmed)) return false;
3349
3466
  if (/Allow access to this file\?/i.test(trimmed)) return false;
3467
+ if (/^(?:Accept this file edit\?|Allow creation of this file\?)$/i.test(trimmed)) return false;
3350
3468
  if (/Do you want to proceed\?/i.test(trimmed)) return false;
3351
3469
  if (/\benter Select\b/i.test(trimmed)) return false;
3352
3470
  if (/^Error ID:\s*[0-9a-fA-F-]*/i.test(trimmed)) {
@@ -3420,12 +3538,15 @@ function isLegacyYNActiveInLines(lines) {
3420
3538
  return { active: true, lineIdx: ynIdx };
3421
3539
  }
3422
3540
  function looksLikePromptSnapshot(snapshot) {
3423
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
3541
+ const stripped = stripAnsiAndExpandRep(snapshot);
3424
3542
  const activeKind = detectActivePromptKind(snapshot);
3425
3543
  if (activeKind !== null) {
3426
3544
  if (activeKind === "system_error") return true;
3427
3545
  const recent = stripped.trimEnd().split("\n").slice(-30).join("\n");
3428
3546
  if (activeKind === "approval") {
3547
+ if (extractFileEditPrompt(snapshot) !== null) {
3548
+ return true;
3549
+ }
3429
3550
  const hasApprovalHeader = /Requesting permission for:/i.test(recent) || /Do you want to proceed\?/i.test(recent);
3430
3551
  const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(recent);
3431
3552
  const hasApprovalFooter = /\besc to cancel\b/i.test(recent);
@@ -3562,14 +3683,333 @@ function extractFileAccessPrompt(snapshot) {
3562
3683
  const fullLine = actionLine ? `Allow access to this file? (${actionLine})` : lines[questionIndex].trim();
3563
3684
  return { identity, fullLine, body };
3564
3685
  }
3686
+ function parseInlineDiffLine(rawLine) {
3687
+ if (/^[>\s❯]*[1-9]\d*[.)]\s+/.test(rawLine)) {
3688
+ return null;
3689
+ }
3690
+ const remMatch = rawLine.match(/^\s*(\d+)\s{1,3}-(?:\s{1,2}(.*)|$)/);
3691
+ if (remMatch) {
3692
+ return {
3693
+ type: "remove",
3694
+ lineNum: Number.parseInt(remMatch[1], 10),
3695
+ content: remMatch[2] ?? ""
3696
+ };
3697
+ }
3698
+ const addMatch = rawLine.match(/^\s*(\d+)\s{1,3}\+(?:\s{1,2}(.*)|$)/);
3699
+ if (addMatch) {
3700
+ return {
3701
+ type: "add",
3702
+ lineNum: Number.parseInt(addMatch[1], 10),
3703
+ content: addMatch[2] ?? ""
3704
+ };
3705
+ }
3706
+ const ctxMatch = rawLine.match(/^\s*(\d+)(?:\s{5}(.*)|\s{3,4}(.*)|\s*)$/);
3707
+ if (ctxMatch) {
3708
+ return {
3709
+ type: "context",
3710
+ lineNum: Number.parseInt(ctxMatch[1], 10),
3711
+ content: ctxMatch[2] ?? ctxMatch[3] ?? ""
3712
+ };
3713
+ }
3714
+ return null;
3715
+ }
3716
+ function isFoldedDiffMarkerLine(trimmed) {
3717
+ 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);
3718
+ }
3719
+ function parseTargetStatSuffix(raw) {
3720
+ const m = raw.match(/^(.*?)\s+((?:\+(\d+)(?:\s+-(\d+))?|-(\d+)))\s*$/);
3721
+ if (!m) {
3722
+ return { cleanPath: raw.trim() };
3723
+ }
3724
+ const added = m[3] !== void 0 ? Number.parseInt(m[3], 10) : m[5] !== void 0 ? 0 : void 0;
3725
+ const removed = m[4] !== void 0 ? Number.parseInt(m[4], 10) : m[5] !== void 0 ? Number.parseInt(m[5], 10) : 0;
3726
+ return {
3727
+ cleanPath: m[1].trim(),
3728
+ stat: m[2].replace(/\s+/g, " ").trim(),
3729
+ added,
3730
+ removed
3731
+ };
3732
+ }
3733
+ function extractFileEditTargetAndStat(line) {
3734
+ if (parseInlineDiffLine(line) !== null) return null;
3735
+ const trimmed = line.trim();
3736
+ if (!trimmed) return null;
3737
+ const actionMatch = trimmed.match(/^(?:New file|Create file|Delete file|Write|Edit|Create):\s*(\S.*)$/i);
3738
+ if (actionMatch) {
3739
+ const parsed2 = parseTargetStatSuffix(actionMatch[1]);
3740
+ if (!parsed2.cleanPath) return null;
3741
+ return {
3742
+ path: parsed2.cleanPath,
3743
+ stat: parsed2.stat,
3744
+ added: parsed2.added,
3745
+ removed: parsed2.removed
3746
+ };
3747
+ }
3748
+ const parsed = parseTargetStatSuffix(trimmed);
3749
+ if (parsed.stat && parsed.cleanPath) {
3750
+ const candidatePath = parsed.cleanPath;
3751
+ 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))) {
3752
+ return {
3753
+ path: candidatePath,
3754
+ stat: parsed.stat,
3755
+ added: parsed.added,
3756
+ removed: parsed.removed
3757
+ };
3758
+ }
3759
+ }
3760
+ return null;
3761
+ }
3762
+ function matchFileEditTargetLine(line) {
3763
+ return extractFileEditTargetAndStat(line)?.path ?? null;
3764
+ }
3765
+ function stitchWrappedPathAbove(lines, fromIndex, initialPath) {
3766
+ let stitched = initialPath;
3767
+ let topIndex = fromIndex;
3768
+ for (let prevIdx = fromIndex - 1; prevIdx >= Math.max(0, fromIndex - 3); prevIdx -= 1) {
3769
+ const prevRaw = lines[prevIdx];
3770
+ const prevTrimmed = prevRaw.trim();
3771
+ if (!prevTrimmed) break;
3772
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) break;
3773
+ if (/^(?:Pending edit|Create file|File access|Question\b|Requesting permission\b)/i.test(prevTrimmed)) break;
3774
+ if (parseInlineDiffLine(prevRaw) !== null || isFoldedDiffMarkerLine(prevTrimmed)) break;
3775
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(prevTrimmed) || /^[│\s]*[❯>]\s*$/.test(prevTrimmed)) break;
3776
+ const actionPrefix = prevTrimmed.match(/^(?:New file|Create file|Delete file|Write|Edit|Create):\s*(\S.*)$/i);
3777
+ if (actionPrefix) {
3778
+ stitched = `${actionPrefix[1].trim()}${stitched}`;
3779
+ topIndex = prevIdx;
3780
+ break;
3781
+ }
3782
+ const isValidPathSegment = !/\s/.test(prevTrimmed) && (prevTrimmed.includes("/") || prevTrimmed.startsWith("~") || prevTrimmed.startsWith(".") || stitched === "" && /\.[a-z0-9]{1,10}$/i.test(prevTrimmed));
3783
+ if (isValidPathSegment && (stitched === "" || prevTrimmed.length >= 40)) {
3784
+ stitched = `${prevTrimmed}${stitched}`;
3785
+ topIndex = prevIdx;
3786
+ if (stitched.startsWith("/") || stitched.startsWith("~/") || stitched.startsWith("./")) {
3787
+ break;
3788
+ }
3789
+ continue;
3790
+ }
3791
+ break;
3792
+ }
3793
+ return { path: stitched, topIndex };
3794
+ }
3795
+ function extractFileEditPrompt(snapshot) {
3796
+ const stripped = stripAnsiAndExpandRep(snapshot);
3797
+ const lines = stripped.trimEnd().split("\n");
3798
+ const lineInFence = computeFenceMask(lines);
3799
+ const windowStart = Math.max(0, lines.length - 30);
3800
+ let questionIndex = -1;
3801
+ for (let i = lines.length - 1; i >= windowStart; i -= 1) {
3802
+ if (lineInFence[i]) continue;
3803
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(lines[i]) && isStandaloneFileEditContext(lines, i, lineInFence)) {
3804
+ questionIndex = i;
3805
+ break;
3806
+ }
3807
+ }
3808
+ if (questionIndex < 0) return null;
3809
+ const questionText = lines[questionIndex].trim();
3810
+ let headingIndex = -1;
3811
+ let headingText = null;
3812
+ let targetIndex = -1;
3813
+ let targetPath = null;
3814
+ let headerStat;
3815
+ let headerAdded;
3816
+ let headerRemoved;
3817
+ let topBodyIndex = questionIndex;
3818
+ const itemsReversed = [];
3819
+ let currentHunkReversed = [];
3820
+ for (let index = questionIndex - 1; index >= 0; index -= 1) {
3821
+ const rawLine = lines[index];
3822
+ const trimmed = rawLine.trim();
3823
+ if (!trimmed) continue;
3824
+ if (/^(?:Pending edit|Create file)$/i.test(trimmed)) {
3825
+ headingIndex = index;
3826
+ headingText = trimmed;
3827
+ topBodyIndex = index + 1;
3828
+ break;
3829
+ }
3830
+ if (/^[─━═_-]{3,}$/.test(trimmed)) {
3831
+ topBodyIndex = index + 1;
3832
+ for (let prevIdx = index - 1; prevIdx >= Math.max(0, index - 4); prevIdx -= 1) {
3833
+ const prevTrimmed = lines[prevIdx].trim();
3834
+ if (!prevTrimmed) continue;
3835
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3836
+ headingIndex = prevIdx;
3837
+ headingText = prevTrimmed;
3838
+ }
3839
+ break;
3840
+ }
3841
+ break;
3842
+ }
3843
+ const diffLine = parseInlineDiffLine(rawLine);
3844
+ if (diffLine && targetIndex < 0) {
3845
+ currentHunkReversed.push(diffLine);
3846
+ topBodyIndex = index;
3847
+ continue;
3848
+ }
3849
+ if (isFoldedDiffMarkerLine(trimmed) && targetIndex < 0) {
3850
+ if (currentHunkReversed.length > 0) {
3851
+ itemsReversed.push({ kind: "hunk", lines: currentHunkReversed.reverse() });
3852
+ currentHunkReversed = [];
3853
+ }
3854
+ itemsReversed.push({ kind: "omitted", marker: trimmed });
3855
+ topBodyIndex = index;
3856
+ continue;
3857
+ }
3858
+ 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)) {
3859
+ topBodyIndex = index;
3860
+ continue;
3861
+ }
3862
+ const extractedTarget = extractFileEditTargetAndStat(rawLine);
3863
+ let matchedTarget = extractedTarget?.path ?? null;
3864
+ let matchedStat = extractedTarget?.stat;
3865
+ let matchedAdded = extractedTarget?.added;
3866
+ let matchedRemoved = extractedTarget?.removed;
3867
+ let targetTopIdx = index;
3868
+ if (!matchedTarget && targetIndex < 0 && /^(?:\+\d+(?:\s+-\d+)?|-\d+)$/.test(trimmed) && index > 0) {
3869
+ const bareStat = parseTargetStatSuffix(`placeholder ${trimmed}`);
3870
+ const stitched = stitchWrappedPathAbove(lines, index, "");
3871
+ if (stitched.path) {
3872
+ matchedTarget = stitched.path;
3873
+ matchedStat = bareStat.stat;
3874
+ matchedAdded = bareStat.added;
3875
+ matchedRemoved = bareStat.removed;
3876
+ targetTopIdx = stitched.topIndex;
3877
+ }
3878
+ } else if (matchedTarget && targetIndex < 0 && !matchedTarget.startsWith("/") && !matchedTarget.startsWith("~/") && !matchedTarget.startsWith("./")) {
3879
+ const stitched = stitchWrappedPathAbove(lines, index, matchedTarget);
3880
+ matchedTarget = stitched.path;
3881
+ targetTopIdx = stitched.topIndex;
3882
+ }
3883
+ if (!matchedTarget && targetIndex < 0 && !/\s/.test(trimmed) && (trimmed.includes("/") || /\.[a-z0-9]{1,8}$/i.test(trimmed)) && !/^[─━═_-]{3,}$/.test(trimmed)) {
3884
+ for (let prevIdx = index - 1; prevIdx >= Math.max(0, index - 4); prevIdx -= 1) {
3885
+ const prevTrimmed = lines[prevIdx].trim();
3886
+ if (!prevTrimmed) continue;
3887
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) continue;
3888
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3889
+ matchedTarget = trimmed;
3890
+ }
3891
+ break;
3892
+ }
3893
+ }
3894
+ if (matchedTarget && targetIndex < 0) {
3895
+ targetIndex = targetTopIdx;
3896
+ targetPath = matchedTarget;
3897
+ headerStat = matchedStat;
3898
+ headerAdded = matchedAdded;
3899
+ headerRemoved = matchedRemoved;
3900
+ topBodyIndex = targetTopIdx;
3901
+ for (let prevIdx = targetTopIdx - 1; prevIdx >= Math.max(0, targetTopIdx - 6); prevIdx -= 1) {
3902
+ const prevTrimmed = lines[prevIdx].trim();
3903
+ if (!prevTrimmed) continue;
3904
+ if (/^[─━═_-]{3,}$/.test(prevTrimmed)) {
3905
+ continue;
3906
+ }
3907
+ if (/^(?:Pending edit|Create file)$/i.test(prevTrimmed)) {
3908
+ headingIndex = prevIdx;
3909
+ headingText = prevTrimmed;
3910
+ }
3911
+ break;
3912
+ }
3913
+ break;
3914
+ }
3915
+ break;
3916
+ }
3917
+ if (currentHunkReversed.length > 0) {
3918
+ itemsReversed.push({ kind: "hunk", lines: currentHunkReversed.reverse() });
3919
+ }
3920
+ const items = itemsReversed.reverse();
3921
+ const hunks = items.filter((item) => item.kind === "hunk").map((item) => item.lines);
3922
+ const omittedMarkers = items.filter((item) => item.kind === "omitted").map((item) => item.marker);
3923
+ const visibleAdded = hunks.reduce(
3924
+ (sum, hunk) => sum + hunk.filter((l) => l.type === "add").length,
3925
+ 0
3926
+ );
3927
+ const visibleRemoved = hunks.reduce(
3928
+ (sum, hunk) => sum + hunk.filter((l) => l.type === "remove").length,
3929
+ 0
3930
+ );
3931
+ const partialReasons = [];
3932
+ if (!targetPath && (hunks.length > 0 || omittedMarkers.length > 0)) {
3933
+ partialReasons.push("file header scrolled off-screen");
3934
+ }
3935
+ if (headerStat && (headerAdded !== void 0 && visibleAdded < headerAdded || headerRemoved !== void 0 && visibleRemoved < headerRemoved)) {
3936
+ partialReasons.push(
3937
+ `showing +${visibleAdded} -${visibleRemoved} of ${headerStat} total changes`
3938
+ );
3939
+ }
3940
+ const allOmissionNotes = [...partialReasons, ...omittedMarkers];
3941
+ const isPartial = allOmissionNotes.length > 0;
3942
+ const omissionNote = isPartial ? allOmissionNotes.join("; ") : void 0;
3943
+ const isCreate = /^Allow creation of this file\?$/i.test(questionText) || /^Create file$/i.test(headingText || "");
3944
+ const identity = targetPath || (isCreate ? "create file" : "file edit");
3945
+ const fullLine = targetPath ? `${questionText} (${targetPath})` : questionText;
3946
+ let body = "";
3947
+ if (hunks.length > 0 || omittedMarkers.length > 0) {
3948
+ const allAdditions = hunks.length > 0 && hunks.every((hunk) => hunk.every((l) => l.type === "add"));
3949
+ const diffOut = ["```diff"];
3950
+ if (targetPath) {
3951
+ const fromHeader = isCreate && allAdditions ? "--- /dev/null" : `--- a/${targetPath}`;
3952
+ const toHeader = `+++ b/${targetPath}`;
3953
+ diffOut.push(fromHeader, toHeader);
3954
+ }
3955
+ if (headerStat) {
3956
+ diffOut.push(`# diff-stat: ${headerStat}`);
3957
+ }
3958
+ for (const reason of partialReasons) {
3959
+ diffOut.push(`# partial-diff: ${reason}`);
3960
+ }
3961
+ for (const item of items) {
3962
+ if (item.kind === "omitted") {
3963
+ diffOut.push(`# omitted: ${item.marker}`);
3964
+ continue;
3965
+ }
3966
+ const hunk = item.lines;
3967
+ const oldCount = hunk.filter((l) => l.type === "remove" || l.type === "context").length;
3968
+ const newCount = hunk.filter((l) => l.type === "add" || l.type === "context").length;
3969
+ const firstOld = hunk.find((l) => l.type === "remove" || l.type === "context")?.lineNum ?? (oldCount === 0 ? 0 : 1);
3970
+ const firstNew = hunk.find((l) => l.type === "add" || l.type === "context")?.lineNum ?? (newCount === 0 ? 0 : 1);
3971
+ diffOut.push(`@@ -${firstOld},${oldCount} +${firstNew},${newCount} @@`);
3972
+ for (const l of hunk) {
3973
+ if (l.type === "remove") {
3974
+ diffOut.push(`-${l.content}`);
3975
+ } else if (l.type === "add") {
3976
+ diffOut.push(`+${l.content}`);
3977
+ } else {
3978
+ diffOut.push(` ${l.content}`);
3979
+ }
3980
+ }
3981
+ }
3982
+ diffOut.push("```");
3983
+ body = diffOut.join("\n");
3984
+ } else {
3985
+ const startIdx = headingIndex >= 0 ? headingIndex + 1 : topBodyIndex;
3986
+ const contextLines = lines.slice(startIdx, questionIndex).map((l) => l.trim()).filter((l) => {
3987
+ if (!l) return false;
3988
+ if (/^(?:Pending edit|Create file)$/i.test(l)) return false;
3989
+ if (/^[─━═_-]{3,}$/.test(l)) return false;
3990
+ if (/^shift\+tab\s+to\s+auto-approve\s+file\s+(?:edits?|writes?)$/i.test(l)) return false;
3991
+ return true;
3992
+ });
3993
+ body = contextLines.join("\n").trim();
3994
+ }
3995
+ return {
3996
+ identity,
3997
+ filePath: targetPath ?? void 0,
3998
+ fullLine,
3999
+ body,
4000
+ questionText,
4001
+ ...headerStat ? { diffStat: headerStat } : {},
4002
+ ...isPartial ? { partial: true, ...omissionNote ? { omissionNote } : {} } : {}
4003
+ };
4004
+ }
3565
4005
  function extractHeader(snapshot) {
3566
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4006
+ const stripped = stripAnsiAndExpandRep(snapshot);
3567
4007
  const matches = Array.from(stripped.matchAll(/Requesting permission for:\s*([^\n]+)/gi));
3568
4008
  if (matches.length === 0) return null;
3569
4009
  return matches[matches.length - 1][1].trim();
3570
4010
  }
3571
4011
  function extractFullHeaderLine(snapshot) {
3572
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4012
+ const stripped = stripAnsiAndExpandRep(snapshot);
3573
4013
  const matches = Array.from(stripped.matchAll(/Requesting permission for:\s*([^\n]+)/gi));
3574
4014
  if (matches.length === 0) return null;
3575
4015
  return matches[matches.length - 1][0].trim();
@@ -3605,7 +4045,7 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3605
4045
  for (let k = boundaryIdx - 1; k >= checkTop; k--) {
3606
4046
  const l = lines[k].trim();
3607
4047
  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)) {
4048
+ 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
4049
  const sliceBetween = lines.slice(k + 1, boundaryIdx);
3610
4050
  let optCount = 0;
3611
4051
  let hasFooter = false;
@@ -3613,6 +4053,10 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3613
4053
  for (const raw of sliceBetween) {
3614
4054
  const s = raw.trim();
3615
4055
  if (!s) continue;
4056
+ if (parseInlineDiffLine(raw) !== null || isFoldedDiffMarkerLine(raw)) {
4057
+ inDiff = true;
4058
+ continue;
4059
+ }
3616
4060
  if (inDiff) {
3617
4061
  if (/^[ +\-\\]/.test(raw)) {
3618
4062
  continue;
@@ -3624,7 +4068,7 @@ function isOpenEnclosingContext(lines, checkTop, topAnchor, boundaryIdx) {
3624
4068
  inDiff = false;
3625
4069
  continue;
3626
4070
  }
3627
- if (/^Do you want to proceed\?/i.test(raw)) {
4071
+ if (/^(?:Do you want to proceed\?|Accept this file edit\?|Allow creation of this file\?)/i.test(raw)) {
3628
4072
  inDiff = false;
3629
4073
  continue;
3630
4074
  }
@@ -3656,7 +4100,7 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3656
4100
  if (/\benter\s+Select\b/i.test(l) || /\benter\s+to\s+(?:select|submit)\b/i.test(l)) {
3657
4101
  return false;
3658
4102
  }
3659
- if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l)) {
4103
+ 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
4104
  return false;
3661
4105
  }
3662
4106
  if (/\b(?:allow|deny)\s+access\b/i.test(l)) {
@@ -3675,7 +4119,7 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3675
4119
  for (let j = questionIdx - 1; j >= start; j--) {
3676
4120
  const l = lines[j].trim();
3677
4121
  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)) {
4122
+ 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
4123
  return false;
3680
4124
  }
3681
4125
  if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
@@ -3722,10 +4166,10 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3722
4166
  if (/Question \d+\/\d+:/i.test(l)) {
3723
4167
  return false;
3724
4168
  }
3725
- if (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l)) {
4169
+ 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
4170
  return false;
3727
4171
  }
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)) {
4172
+ 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
4173
  return false;
3730
4174
  }
3731
4175
  if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l) || /^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
@@ -3737,11 +4181,100 @@ function isStandaloneFileAccessContext(lines, questionIdx) {
3737
4181
  }
3738
4182
  return true;
3739
4183
  }
4184
+ function isStandaloneFileEditContext(lines, questionIdx, fenceMask) {
4185
+ if (fenceMask && fenceMask[questionIdx]) {
4186
+ return false;
4187
+ }
4188
+ const windowEnd = Math.min(lines.length, questionIdx + 15);
4189
+ let optionCount = 0;
4190
+ let hasFileEditOption = false;
4191
+ let hasFooter = false;
4192
+ let promptEndIdx = questionIdx;
4193
+ for (let j = questionIdx + 1; j < windowEnd; j++) {
4194
+ if (fenceMask && fenceMask[j]) continue;
4195
+ const raw = lines[j];
4196
+ const l = raw.trim();
4197
+ if (!l) continue;
4198
+ if (/\benter\s+Select\b/i.test(l) || /\benter\s+to\s+(?:select|submit)\b/i.test(l)) {
4199
+ return false;
4200
+ }
4201
+ if (/Do you want to proceed\?/i.test(l) || /Requesting permission for:/i.test(l) || /Allow access to this file\?/i.test(l)) {
4202
+ return false;
4203
+ }
4204
+ if (isAgyStatusFooterLine(l) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(l) || isComposerBoxAt(lines, j)) {
4205
+ if (/^esc to cancel\b/i.test(l)) {
4206
+ hasFooter = true;
4207
+ }
4208
+ break;
4209
+ }
4210
+ if (optionCount > 0 && isResumedTurnSignal(l)) {
4211
+ break;
4212
+ }
4213
+ if (/^[>\s❯]*[1-9]\.\s+\S/.test(l)) {
4214
+ optionCount++;
4215
+ promptEndIdx = j;
4216
+ if (/\b(?:accept|reject|allow|deny|create|change|file|yes|no)\b/i.test(l)) {
4217
+ hasFileEditOption = true;
4218
+ }
4219
+ continue;
4220
+ }
4221
+ 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)) {
4222
+ hasFooter = true;
4223
+ promptEndIdx = j;
4224
+ continue;
4225
+ }
4226
+ if (/^[─━│╭╰╮╯\s]*$/.test(l) && l.length > 5) {
4227
+ break;
4228
+ }
4229
+ if (/^[▶▸\s]*Thought|^●\s*/i.test(l) || /^[│\s]*[❯>]\s*$/.test(l)) {
4230
+ break;
4231
+ }
4232
+ }
4233
+ if (optionCount < 2 || !hasFileEditOption || !hasFooter) {
4234
+ return false;
4235
+ }
4236
+ if (hasTrailingAgyComposer(lines, promptEndIdx, fenceMask)) {
4237
+ return false;
4238
+ }
4239
+ const checkTop = Math.max(0, questionIdx - 120);
4240
+ let hasStructuralEditAnchor = false;
4241
+ for (let j = questionIdx - 1; j >= checkTop; j--) {
4242
+ const raw = lines[j];
4243
+ const l = raw.trim();
4244
+ if (!l) continue;
4245
+ 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)) {
4246
+ hasStructuralEditAnchor = true;
4247
+ continue;
4248
+ }
4249
+ if (/^(?:Pending edit|Create file)$/i.test(l)) {
4250
+ hasStructuralEditAnchor = true;
4251
+ break;
4252
+ }
4253
+ if (parseInlineDiffLine(raw) !== null || isFoldedDiffMarkerLine(l)) {
4254
+ hasStructuralEditAnchor = true;
4255
+ continue;
4256
+ }
4257
+ if (/^Reason:\s*\S/i.test(l) || /^[⚠⚠️]\s*/.test(l) || /^Esc disabled during file edits\b/i.test(l)) {
4258
+ continue;
4259
+ }
4260
+ const targetCandidate = matchFileEditTargetLine(raw);
4261
+ if (targetCandidate) {
4262
+ hasStructuralEditAnchor = true;
4263
+ break;
4264
+ }
4265
+ if (hasStructuralEditAnchor) {
4266
+ break;
4267
+ }
4268
+ return false;
4269
+ }
4270
+ return hasStructuralEditAnchor;
4271
+ }
3740
4272
  function detectActivePromptKind(snapshot) {
3741
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4273
+ const stripped = stripAnsiAndExpandRep(snapshot);
3742
4274
  const lines = stripped.trimEnd().split("\n");
3743
4275
  const windowStart = Math.max(0, lines.length - 30);
3744
4276
  const approvalRe = /Requesting permission for:/i;
4277
+ const fileEditQuestionRe = /^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i;
3745
4278
  const fileAccessRe = /^\s*File access\s*$/i;
3746
4279
  const fileAccessQuestionRe = /^\s*Allow access to this file\?\s*$/i;
3747
4280
  const lineInFence = computeFenceMask(lines);
@@ -3752,8 +4285,10 @@ function detectActivePromptKind(snapshot) {
3752
4285
  for (let i = lines.length - 1; i >= 0; i--) {
3753
4286
  const line = lines[i];
3754
4287
  if (i >= windowStart) {
3755
- if (approvalRe.test(line)) {
3756
- if (!lineInFence[i] && lastApprovalLineIdx < 0) {
4288
+ if (lastApprovalLineIdx < 0 && !lineInFence[i]) {
4289
+ if (approvalRe.test(line)) {
4290
+ lastApprovalLineIdx = i;
4291
+ } else if (fileEditQuestionRe.test(line) && isStandaloneFileEditContext(lines, i, lineInFence)) {
3757
4292
  lastApprovalLineIdx = i;
3758
4293
  }
3759
4294
  }
@@ -3828,7 +4363,9 @@ function detectActivePromptKind(snapshot) {
3828
4363
  }
3829
4364
  } else if (latestChooserIdx === lastApprovalLineIdx) {
3830
4365
  const hasApprovalOptions = /^[>\s]*[1-9]\. (?:Yes|No)\b/im.test(remainingLines);
3831
- const hasApprovalFooter = /\besc to cancel\b/i.test(remainingLines);
4366
+ 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(
4367
+ remainingLines
4368
+ );
3832
4369
  if (hasApprovalOptions && hasApprovalFooter) {
3833
4370
  return "approval";
3834
4371
  }
@@ -4134,7 +4671,7 @@ function isDurationToken(word) {
4134
4671
  return /^\d+[dhms](?:\d+[dhms])*$/i.test(word);
4135
4672
  }
4136
4673
  function extractSystemErrorPrompt(snapshot) {
4137
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4674
+ const stripped = stripAnsiAndExpandRep(snapshot);
4138
4675
  const lines = stripped.split("\n");
4139
4676
  let bannerIdx = -1;
4140
4677
  let rawMessage = "";
@@ -4240,7 +4777,7 @@ function isQuestionFooterLine(line) {
4240
4777
  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
4778
  }
4242
4779
  function extractQuestionHeader(snapshot) {
4243
- const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
4780
+ const stripped = stripAnsiAndExpandRep(snapshot);
4244
4781
  const lines = stripped.split("\n");
4245
4782
  const lineInFence = computeFenceMask(lines);
4246
4783
  const prefixRe = /^\s*(?:[❯>]\s*)?(?:\?\s*(?:Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)?|Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:\s*)/i;
@@ -4257,9 +4794,17 @@ function extractQuestionHeader(snapshot) {
4257
4794
  return null;
4258
4795
  }
4259
4796
  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();
4261
- if (detectActivePromptKind(normalized) === "system_error") {
4797
+ let normalized = stripAnsiAndExpandRep(snapshot).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
4798
+ const activeKind = detectActivePromptKind(normalized);
4799
+ if (activeKind === "system_error") {
4262
4800
  normalized = normalized.replace(/\bResets\s+in\s+\d+[dhms](?:\s*\d+[dhms])*\.?/gi, "Resets in <duration>");
4801
+ } else if (activeKind === "approval") {
4802
+ const fileEdit = extractFileEditPrompt(snapshot);
4803
+ if (fileEdit?.body) {
4804
+ normalized = `${normalized}
4805
+ @@file-edit-body@@
4806
+ ${fileEdit.body}`;
4807
+ }
4263
4808
  }
4264
4809
  return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
4265
4810
  }
@@ -4304,14 +4849,37 @@ function parseApprovalSnapshot(snapshot) {
4304
4849
  return parseSystemErrorSnapshot(stripped, snapshot, systemError);
4305
4850
  }
4306
4851
  }
4852
+ if (kind === "approval") {
4853
+ const lines = stripped.trimEnd().split("\n");
4854
+ const windowStart = Math.max(0, lines.length - 30);
4855
+ let lastCmdApprovalIdx = -1;
4856
+ let lastFileEditIdx = -1;
4857
+ for (let i = lines.length - 1; i >= windowStart; i -= 1) {
4858
+ const l = lines[i];
4859
+ if (lastCmdApprovalIdx < 0 && (/Requesting permission for:/i.test(l) || /Do you want to proceed\?/i.test(l))) {
4860
+ lastCmdApprovalIdx = i;
4861
+ }
4862
+ if (lastFileEditIdx < 0 && /^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(l)) {
4863
+ lastFileEditIdx = i;
4864
+ }
4865
+ }
4866
+ if (lastFileEditIdx > lastCmdApprovalIdx) {
4867
+ const fileEdit = extractFileEditPrompt(snapshot);
4868
+ if (fileEdit) {
4869
+ return parseFileEditSnapshot(stripped, snapshot, fileEdit);
4870
+ }
4871
+ }
4872
+ }
4307
4873
  return parseApprovalUISnapshot(stripped, snapshot);
4308
4874
  }
4309
4875
  function buildApprovalSemanticKey(candidate) {
4310
4876
  const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
4877
+ const rawBody = (candidate.body ?? "").replace(/\r\n/g, "\n");
4878
+ const isDiffBody = /```diff\b/i.test(rawBody) || /^(?:Accept this file edit\?|Allow creation of this file\?)/i.test(candidate.fullHeaderLine ?? "");
4311
4879
  const payload = {
4312
4880
  kind: candidate.kind,
4313
4881
  headerText: normalize(candidate.headerText),
4314
- body: normalize(candidate.body),
4882
+ body: isDiffBody ? rawBody.trim() : normalize(rawBody),
4315
4883
  options: (candidate.options ?? []).map((option) => ({
4316
4884
  number: option.number,
4317
4885
  text: normalize(option.text)
@@ -4322,6 +4890,13 @@ function buildApprovalSemanticKey(candidate) {
4322
4890
  };
4323
4891
  return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
4324
4892
  }
4893
+ function buildApprovalRedrawSemanticKey(candidate) {
4894
+ const rightTrimmedBody = (candidate.body ?? "").replace(/\r\n/g, "\n").split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n");
4895
+ return buildApprovalSemanticKey({
4896
+ ...candidate,
4897
+ body: rightTrimmedBody
4898
+ });
4899
+ }
4325
4900
  function hasNegativeApprovalOption(options) {
4326
4901
  return options.some((option) => /^(?:no|deny|reject|cancel)\b/i.test(option.text.trim()));
4327
4902
  }
@@ -4349,6 +4924,30 @@ function parseFileAccessSnapshot(stripped, originalSnapshot, prompt) {
4349
4924
  paneHash: hashPromptSnapshot(originalSnapshot)
4350
4925
  };
4351
4926
  }
4927
+ function parseFileEditSnapshot(stripped, originalSnapshot, prompt) {
4928
+ const lines = stripped.split("\n");
4929
+ let questionIndex = -1;
4930
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
4931
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(lines[index])) {
4932
+ questionIndex = index;
4933
+ break;
4934
+ }
4935
+ }
4936
+ const belowQuestion = questionIndex >= 0 ? lines.slice(questionIndex + 1).join("\n") : stripped;
4937
+ const options = parseOptions(belowQuestion);
4938
+ if (options.length < 2 || !hasNegativeApprovalOption(options)) return null;
4939
+ return {
4940
+ kind: "approval",
4941
+ headerText: prompt.identity,
4942
+ fullHeaderLine: prompt.fullLine,
4943
+ command: void 0,
4944
+ filePath: prompt.filePath,
4945
+ options,
4946
+ submitMap: buildApprovalSubmitMap(options),
4947
+ body: prompt.body,
4948
+ paneHash: hashPromptSnapshot(originalSnapshot)
4949
+ };
4950
+ }
4352
4951
  function parseSystemErrorSnapshot(stripped, originalSnapshot, prompt) {
4353
4952
  return {
4354
4953
  kind: "system_error",
@@ -4478,7 +5077,7 @@ function buildQuestionSubmitMap(options) {
4478
5077
  function isWriteInInputActive(paneText, expectedHeader, expectedStep, expectedBodyText) {
4479
5078
  if (!paneText) return false;
4480
5079
  if (detectActivePromptKind(paneText) !== "question") return false;
4481
- const stripped = paneText.replace(ANSI_ESCAPE_REGEX2, "");
5080
+ const stripped = stripAnsiAndExpandRep(paneText);
4482
5081
  const lines = stripped.trimEnd().split("\n");
4483
5082
  const windowStart = Math.max(0, lines.length - 30);
4484
5083
  let headerIdx = -1;
@@ -4611,12 +5210,13 @@ function extractIdentity(headerText) {
4611
5210
  function guessToolTypeFromIdentity(c) {
4612
5211
  if (c.kind === "question") return "ASK_QUESTION";
4613
5212
  if (c.command) return "RUN_COMMAND";
4614
- if (c.filePath) return "CODE_ACTION";
5213
+ if (c.filePath || c.headerText === "file edit" || c.headerText === "create file" || c.headerText === "file creation") {
5214
+ return "CODE_ACTION";
5215
+ }
4615
5216
  return "GENERIC";
4616
5217
  }
4617
- var ANSI_ESCAPE_REGEX2 = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
4618
5218
  function stripAnsi(s) {
4619
- return s.replace(ANSI_ESCAPE_REGEX2, "");
5219
+ return stripAnsiAndExpandRep(s);
4620
5220
  }
4621
5221
 
4622
5222
  // src/event-mapper.ts
@@ -4806,6 +5406,10 @@ function hookToolNameToTranscriptType(hookName) {
4806
5406
  switch (upper) {
4807
5407
  case "LIST_DIR":
4808
5408
  return "LIST_DIRECTORY";
5409
+ case "READ_FILE":
5410
+ return "VIEW_FILE";
5411
+ case "EDIT_FILE":
5412
+ case "WRITE_FILE":
4809
5413
  case "REPLACE_FILE_CONTENT":
4810
5414
  case "MULTI_REPLACE_FILE_CONTENT":
4811
5415
  case "WRITE_TO_FILE":
@@ -4842,6 +5446,8 @@ function extractFilePathFromArgs(toolName, args) {
4842
5446
  if (!args || typeof args !== "object") return void 0;
4843
5447
  const a = args;
4844
5448
  for (const key of [
5449
+ "Path",
5450
+ "AbsolutePath",
4845
5451
  "TargetFile",
4846
5452
  "FilePath",
4847
5453
  "file_path",
@@ -5266,6 +5872,32 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5266
5872
  });
5267
5873
  return null;
5268
5874
  }
5875
+ if (candidate.kind === "approval") {
5876
+ const candidateSemanticKey = buildApprovalSemanticKey(candidate);
5877
+ const candidateRedrawSemanticKey = buildApprovalRedrawSemanticKey(candidate);
5878
+ const existing = [...this.pendingPrompts.values()].find(
5879
+ (p) => p.conversationId === conversationId && p.kind === candidate.kind && (p.paneSemanticKey === candidateSemanticKey || p.paneRedrawSemanticKey !== void 0 && p.paneRedrawSemanticKey === candidateRedrawSemanticKey)
5880
+ );
5881
+ if (existing) {
5882
+ if (existing.paneHash && existing.paneHash !== candidate.paneHash) {
5883
+ this.paneOnlyEmittedHashes.delete(existing.paneHash);
5884
+ }
5885
+ existing.paneHash = candidate.paneHash;
5886
+ if (candidate.paneHash) {
5887
+ this.paneOnlyEmittedHashes.add(candidate.paneHash);
5888
+ }
5889
+ existing.paneSemanticKey = candidateSemanticKey;
5890
+ existing.paneRedrawSemanticKey = candidateRedrawSemanticKey;
5891
+ existing.body = candidate.body;
5892
+ logger.debug("Skipping pane-only re-emit for active approval with matching semantic key", {
5893
+ promptId: existing.promptId,
5894
+ paneSemanticKey: candidateSemanticKey.substring(0, 16),
5895
+ paneRedrawSemanticKey: candidateRedrawSemanticKey.substring(0, 16),
5896
+ paneHash: candidate.paneHash.substring(0, 16)
5897
+ });
5898
+ return null;
5899
+ }
5900
+ }
5269
5901
  if (candidate.kind === "question") {
5270
5902
  const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
5271
5903
  const candidateSemanticKey = buildApprovalSemanticKey(candidate);
@@ -5478,7 +6110,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5478
6110
  toolType: matchingCall?.toolType ?? guessToolTypeFromIdentity(candidate),
5479
6111
  command: matchingCall?.command ?? candidate.command,
5480
6112
  filePath: matchingCall?.filePath ?? candidate.filePath,
5481
- rawToolName: matchingCall?.rawToolName ?? (candidate.kind === "question" ? "ask_question" : candidate.command ?? candidate.filePath ?? "unknown"),
6113
+ 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
6114
  questionText: synthLineMatch?.stitchedHeaderText ?? (candidate.kind === "question" && candidate.headerText ? candidate.headerText : matchingCall?.questionText),
5483
6115
  // NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
5484
6116
  // that directly correspond to desktop keystrokes (submitMap). Transcript options
@@ -5502,6 +6134,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5502
6134
  body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
5503
6135
  paneOptions: candidate.options,
5504
6136
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6137
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
5505
6138
  kind: candidate.kind,
5506
6139
  isHookPrompt: isHookCall,
5507
6140
  // Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
@@ -5822,6 +6455,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5822
6455
  body: candidate.body,
5823
6456
  paneOptions: candidate.options,
5824
6457
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6458
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
5825
6459
  kind: candidate.kind,
5826
6460
  emittedAt: Date.now(),
5827
6461
  ttlMs: this.promptTtlMs
@@ -6800,8 +7434,11 @@ function friendlyHookToolName(rawName, resultType) {
6800
7434
  return "LS";
6801
7435
  case "GREP_SEARCH":
6802
7436
  return "Grep";
7437
+ case "READ_FILE":
6803
7438
  case "VIEW_FILE":
6804
7439
  return "Read";
7440
+ case "EDIT_FILE":
7441
+ case "WRITE_FILE":
6805
7442
  case "WRITE_TO_FILE":
6806
7443
  case "REPLACE_FILE_CONTENT":
6807
7444
  case "MULTI_REPLACE_FILE_CONTENT":
@@ -9494,14 +10131,19 @@ var McpServer = class _McpServer {
9494
10131
  });
9495
10132
  }
9496
10133
  const toolName = diffParsed ? "Edit" : state.pendingCall?.toolType;
9497
- const filePath = diffParsed?.filePath ?? state.pendingCall?.filePath;
10134
+ const filePath = state.pendingCall?.filePath ?? diffParsed?.filePath;
9498
10135
  const toolInput = diffParsed ? {
9499
- file_path: diffParsed.filePath,
10136
+ file_path: filePath,
9500
10137
  old_string: diffParsed.oldString,
9501
10138
  new_string: diffParsed.newString,
9502
10139
  old_start_line: diffParsed.oldStartLine,
9503
10140
  new_start_line: diffParsed.newStartLine,
9504
- diff: diffParsed.rawDiff
10141
+ diff: diffParsed.rawDiff,
10142
+ ...diffParsed.diffStat ? { diff_stat: diffParsed.diffStat } : {},
10143
+ ...diffParsed.partial ? {
10144
+ partial: true,
10145
+ ...diffParsed.omissionNote ? { omission_note: diffParsed.omissionNote } : {}
10146
+ } : {}
9505
10147
  } : void 0;
9506
10148
  const optionsForMobile = state.pendingCall ? {
9507
10149
  promptId: state.promptId,
@@ -9535,7 +10177,8 @@ var McpServer = class _McpServer {
9535
10177
  })()
9536
10178
  } : {}
9537
10179
  } : { promptId: state.promptId };
9538
- const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
10180
+ const baseContent = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
10181
+ const content = diffParsed?.partial && diffParsed.omissionNote && !baseContent.includes(diffParsed.omissionNote) ? `${baseContent} \u2014 partial diff (${diffParsed.omissionNote})` : baseContent;
9539
10182
  const input = {
9540
10183
  sessionId: session.sessionId,
9541
10184
  type: import_codevibe_core7.EventType.INTERACTIVE_PROMPT,
@@ -10451,6 +11094,9 @@ function parseFencedDiffFromBody(body, header) {
10451
11094
  const rawDiff = fenceMatch[1];
10452
11095
  const oldLines = [];
10453
11096
  const newLines = [];
11097
+ const omissionNotes = [];
11098
+ let diffStat;
11099
+ let partial = false;
10454
11100
  const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
10455
11101
  let currentOldLine = 0;
10456
11102
  let currentNewLine = 0;
@@ -10465,6 +11111,17 @@ function parseFencedDiffFromBody(body, header) {
10465
11111
  return p;
10466
11112
  };
10467
11113
  for (const line of rawDiff.split("\n")) {
11114
+ const statComment = line.match(/^#\s*diff-stat:\s*((?:\+\d+(?:\s+-\d+)?|-\d+))\s*$/i);
11115
+ if (statComment) {
11116
+ diffStat = statComment[1].replace(/\s+/g, " ").trim();
11117
+ continue;
11118
+ }
11119
+ const omissionComment = line.match(/^#\s*(?:partial-diff|omitted):\s*(.+)$/i);
11120
+ if (omissionComment) {
11121
+ partial = true;
11122
+ omissionNotes.push(omissionComment[1].trim());
11123
+ continue;
11124
+ }
10468
11125
  const hunkMatch = line.match(hunkHeaderPattern);
10469
11126
  if (hunkMatch) {
10470
11127
  hunkCount += 1;
@@ -10472,12 +11129,12 @@ function parseFencedDiffFromBody(body, header) {
10472
11129
  currentNewLine = Number.parseInt(hunkMatch[2], 10);
10473
11130
  continue;
10474
11131
  }
10475
- if (line.startsWith("+++ ")) {
11132
+ if (hunkCount === 0 && line.startsWith("+++ ")) {
10476
11133
  const pathVal = stripPrefix(line.slice(4));
10477
11134
  if (pathVal) filePath = pathVal;
10478
11135
  continue;
10479
11136
  }
10480
- if (line.startsWith("--- ")) {
11137
+ if (hunkCount === 0 && line.startsWith("--- ")) {
10481
11138
  if (!filePath) {
10482
11139
  const pathVal = stripPrefix(line.slice(4));
10483
11140
  if (pathVal) filePath = pathVal;
@@ -10504,8 +11161,9 @@ function parseFencedDiffFromBody(body, header) {
10504
11161
  oldStartLine = void 0;
10505
11162
  newStartLine = void 0;
10506
11163
  }
11164
+ const isFileEditHeader = /^(?:Accept this file edit\?|Allow creation of this file\?)/i.test(header ?? "");
10507
11165
  if (!filePath) {
10508
- const searchTargets = [body, header ?? ""];
11166
+ const searchTargets = isFileEditHeader ? [header ?? ""] : [body, header ?? ""];
10509
11167
  for (const target of searchTargets) {
10510
11168
  if (!target) continue;
10511
11169
  const m = target.match(/\[.*?\]\(file:\/\/([^\s\)]+)\)/) || target.match(/file:\/\/([^\s\)]+)/);
@@ -10525,7 +11183,12 @@ function parseFencedDiffFromBody(body, header) {
10525
11183
  newString: newLines.join("\n"),
10526
11184
  oldStartLine,
10527
11185
  newStartLine,
10528
- rawDiff
11186
+ rawDiff,
11187
+ ...diffStat ? { diffStat } : {},
11188
+ ...partial ? {
11189
+ partial: true,
11190
+ ...omissionNotes.length > 0 ? { omissionNote: omissionNotes.join("; ") } : {}
11191
+ } : {}
10529
11192
  };
10530
11193
  }
10531
11194
  var __testing = {
@@ -10550,6 +11213,7 @@ var __testing = {
10550
11213
  commandDigest,
10551
11214
  detectActivePromptKind,
10552
11215
  extractFileAccessPrompt,
11216
+ extractFileEditPrompt,
10553
11217
  extractSystemErrorPrompt,
10554
11218
  generateLaunchSessionId,
10555
11219
  getActiveConversationFromCliLog,
@@ -10564,5 +11228,6 @@ var __testing = {
10564
11228
  readTranscriptFirstType,
10565
11229
  reconstructFooter,
10566
11230
  runForeignSpawnIdle,
10567
- safeIsoTimestamp
11231
+ safeIsoTimestamp,
11232
+ stripAnsiAndExpandRep
10568
11233
  });
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.102",
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",