@quantiya/codevibe-antigravity-plugin 2.0.101 → 2.0.103

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
@@ -100,8 +100,32 @@ function decodeBoolean(val) {
100
100
  if (typeof val === "number") return val !== 0;
101
101
  return false;
102
102
  }
103
+ function sliceToSerializedBudget(rawText, buildCandidate, maxChars) {
104
+ if (maxChars <= 0) return null;
105
+ const emptyCandidate = buildCandidate("");
106
+ if (emptyCandidate.length > maxChars) {
107
+ return "[]".length <= maxChars ? "[]" : null;
108
+ }
109
+ const fullCandidate = buildCandidate(rawText);
110
+ if (fullCandidate.length <= maxChars) return fullCandidate;
111
+ let low = 0;
112
+ let high = rawText.length;
113
+ let best = emptyCandidate;
114
+ while (low <= high) {
115
+ const mid = low + high >>> 1;
116
+ const candidate = buildCandidate(rawText.slice(0, mid));
117
+ if (candidate.length <= maxChars) {
118
+ best = candidate;
119
+ low = mid + 1;
120
+ } else {
121
+ high = mid - 1;
122
+ }
123
+ }
124
+ return best;
125
+ }
103
126
  function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyHint) {
104
127
  try {
128
+ if (maxChars <= 0) return null;
105
129
  let parsed = value;
106
130
  if (typeof value === "string") {
107
131
  const trimmed = value.trim();
@@ -121,15 +145,15 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
121
145
  );
122
146
  if (isOptionArray) {
123
147
  const boundedOptions = parsed.slice(0, 30).map((opt, idx) => {
124
- if (typeof opt === "string") return { number: String(idx + 1), text: opt.slice(0, 256) };
148
+ if (typeof opt === "string") return { number: String(idx + 1), text: opt };
125
149
  if (typeof opt === "number") return { number: String(idx + 1), text: String(opt) };
126
150
  if (opt && typeof opt === "object") {
127
151
  const optObj = opt;
128
152
  const num = typeof optObj.number !== "undefined" ? String(optObj.number).slice(0, 16) : String(idx + 1);
129
- const txt = typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256);
153
+ const txt = typeof optObj.text === "string" ? optObj.text : String(optObj.text ?? optObj.label ?? optObj.value ?? "");
130
154
  return { number: num, text: txt };
131
155
  }
132
- return { number: String(idx + 1), text: String(opt ?? "").slice(0, 256) };
156
+ return { number: String(idx + 1), text: String(opt ?? "") };
133
157
  });
134
158
  const str2 = JSON.stringify(boundedOptions);
135
159
  if (str2.length <= maxChars) return str2;
@@ -139,38 +163,41 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
139
163
  }
140
164
  const firstOpt = boundedOptions[0];
141
165
  if (firstOpt) {
142
- const safeText = firstOpt.text.slice(0, Math.max(0, maxChars - 30));
143
- return JSON.stringify([{ number: firstOpt.number, text: safeText }]);
166
+ return sliceToSerializedBudget(
167
+ firstOpt.text,
168
+ (safeText) => JSON.stringify([{ number: firstOpt.number, text: safeText }]),
169
+ maxChars
170
+ );
144
171
  }
145
- return JSON.stringify(boundedOptions.slice(0, 1));
172
+ return "[]".length <= maxChars ? "[]" : null;
146
173
  }
147
174
  const bounded = parsed.slice(0, 20).map((item) => {
148
- if (typeof item === "string") return { question: item.slice(0, 1024), options: [] };
175
+ if (typeof item === "string") return { question: item, options: [] };
149
176
  if (item && typeof item === "object") {
150
177
  const res = {};
151
178
  const obj = item;
152
- if (typeof obj.question === "string") res.question = obj.question.slice(0, 1024);
179
+ if (typeof obj.question === "string") res.question = obj.question;
153
180
  if (typeof obj.is_multi_select !== "undefined") res.is_multi_select = decodeBoolean(obj.is_multi_select);
154
181
  if (typeof obj.isMultiSelect !== "undefined") res.isMultiSelect = decodeBoolean(obj.isMultiSelect);
155
182
  if (Array.isArray(obj.options)) {
156
- res.options = obj.options.slice(0, 25).map((opt, idx) => {
157
- if (typeof opt === "string") return opt.slice(0, 256);
183
+ res.options = obj.options.slice(0, 25).map((opt) => {
184
+ if (typeof opt === "string") return opt;
158
185
  if (typeof opt === "number") return String(opt);
159
186
  if (opt && typeof opt === "object") {
160
187
  const optObj = opt;
161
188
  return {
162
189
  ...typeof optObj.number !== "undefined" ? { number: String(optObj.number).slice(0, 16) } : {},
163
- text: typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256)
190
+ text: typeof optObj.text === "string" ? optObj.text : String(optObj.text ?? optObj.label ?? optObj.value ?? "")
164
191
  };
165
192
  }
166
- return String(opt ?? "").slice(0, 256);
193
+ return String(opt ?? "");
167
194
  });
168
195
  }
169
196
  return res;
170
197
  }
171
- return { question: String(item).slice(0, 1024), options: [] };
198
+ return { question: String(item), options: [] };
172
199
  });
173
- let str = JSON.stringify(bounded);
200
+ const str = JSON.stringify(bounded);
174
201
  if (str.length <= maxChars) return str;
175
202
  for (let i = bounded.length - 1; i >= 1; i--) {
176
203
  const sub = JSON.stringify(bounded.slice(0, i));
@@ -178,11 +205,20 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
178
205
  }
179
206
  const firstQ = bounded[0];
180
207
  if (firstQ && typeof firstQ === "object") {
181
- const qText = typeof firstQ.question === "string" ? firstQ.question : "";
182
- const safeQText = qText.slice(0, Math.max(0, maxChars - 30));
183
- return JSON.stringify([{ question: safeQText, options: [] }]);
208
+ const qObj = firstQ;
209
+ const qText = typeof qObj.question === "string" ? qObj.question : "";
210
+ const opts = Array.isArray(qObj.options) ? qObj.options : [];
211
+ for (let k = opts.length - 1; k >= 1; k--) {
212
+ const subQ = JSON.stringify([{ ...qObj, options: opts.slice(0, k) }]);
213
+ if (subQ.length <= maxChars) return subQ;
214
+ }
215
+ return sliceToSerializedBudget(
216
+ qText,
217
+ (safeQText) => JSON.stringify([{ question: safeQText, options: [] }]),
218
+ maxChars
219
+ );
184
220
  }
185
- return JSON.stringify(bounded.slice(0, 1));
221
+ return "[]".length <= maxChars ? "[]" : null;
186
222
  }
187
223
  if (parsed && typeof parsed === "object") {
188
224
  const raw = JSON.stringify(parsed);
@@ -216,6 +252,8 @@ function buildBody(raw) {
216
252
  if (key === "questions" || key === "options") {
217
253
  const serialized = serializeStructuredArg(value, MAX_STRUCTURED_ARG_CHARS, key);
218
254
  if (serialized !== null) kept[key] = serialized;
255
+ } else if (key === "question" || key === "prompt") {
256
+ kept[key] = value.slice(0, MAX_STRUCTURED_ARG_CHARS);
219
257
  } else {
220
258
  kept[key] = value.slice(0, MAX_TOOL_ARG_CHARS);
221
259
  }
package/dist/server.js CHANGED
@@ -2448,6 +2448,10 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2448
2448
  * start()/stop() lifecycle and doesn't distinguish debounce
2449
2449
  * cycles. (Stage 2 iter-4 R1 HIGH finding 2026-05-21.) */
2450
2450
  this.debounceCycleId = 0;
2451
+ this.clearDebounceTimer = null;
2452
+ this.clearDebounceWindowOpenedAt = null;
2453
+ this.clearDebounceCycleId = 0;
2454
+ this.clearFlushInFlight = false;
2451
2455
  this.pipeFilePath = options.pipeFilePath ?? path6.join(os5.tmpdir(), `codevibe-agy-pane-${process.pid}.log`);
2452
2456
  this.skipTmuxCommands = options.skipTmuxCommands ?? false;
2453
2457
  this.captureFn = options.captureFn ?? null;
@@ -2482,6 +2486,9 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2482
2486
  * and they paint atomically. */
2483
2487
  this.QUESTION_SETTLE_DELAY_MS = 100;
2484
2488
  }
2489
+ static {
2490
+ this.CLEAR_CONFIRM_DELAY_MS = 50;
2491
+ }
2485
2492
  /**
2486
2493
  * Start observing the tmux pane targeted by `sessionName`. If sessionName
2487
2494
  * is omitted, read $CODEVIBE_AGY_TMUX_TARGET from env (set by the
@@ -2707,6 +2714,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2707
2714
  * goes idle (e.g. an animated spinner inside the approval UI).
2708
2715
  */
2709
2716
  scheduleDebouncedEmit(candidate) {
2717
+ this.cancelClearDebounce();
2710
2718
  const now = Date.now();
2711
2719
  this.pendingEmitCandidate = candidate;
2712
2720
  if (this.debounceWindowOpenedAt === null) {
@@ -2754,7 +2762,10 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2754
2762
  let candidate = staged;
2755
2763
  try {
2756
2764
  const freshSnapshot = await this.captureSnapshot();
2765
+ if (this.generation !== scheduledGeneration || !this.started) return;
2766
+ if (this.debounceCycleId !== scheduledCycleId) return;
2757
2767
  if (freshSnapshot && looksLikePromptSnapshot(freshSnapshot)) {
2768
+ this.cancelClearDebounce();
2758
2769
  candidate = {
2759
2770
  rawDelta: staged.rawDelta,
2760
2771
  snapshot: freshSnapshot,
@@ -2774,6 +2785,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2774
2785
  const settledHeader = settled ? extractQuestionHeader(settled) : null;
2775
2786
  const sameBody = !!firstHeader && !!settledHeader && firstHeader.body === settledHeader.body;
2776
2787
  if (settled && looksLikePromptSnapshot(settled) && detectActivePromptKind(settled) === "question" && sameBody) {
2788
+ this.cancelClearDebounce();
2777
2789
  candidate = {
2778
2790
  rawDelta: staged.rawDelta,
2779
2791
  snapshot: settled,
@@ -2825,6 +2837,64 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2825
2837
  this.pendingEmitCandidate = null;
2826
2838
  this.debounceWindowOpenedAt = null;
2827
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
+ }
2828
2898
  }
2829
2899
  /**
2830
2900
  * Public hook for tests / drivers: trigger a read+process cycle as if
@@ -2846,6 +2916,11 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2846
2916
  this.debounceTimer = null;
2847
2917
  await this.flushDebouncedEmit(this.generation, this.debounceCycleId);
2848
2918
  }
2919
+ if (this.clearDebounceTimer) {
2920
+ clearTimeout(this.clearDebounceTimer);
2921
+ this.clearDebounceTimer = null;
2922
+ await this.flushDebouncedClear(this.generation, this.clearDebounceCycleId);
2923
+ }
2849
2924
  }
2850
2925
  async processFileChanges() {
2851
2926
  if (this.processing) {
@@ -2863,7 +2938,7 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2863
2938
  if (!chunk) continue;
2864
2939
  if (!looksLikePromptDelta(chunk)) {
2865
2940
  if (this.approvalUIActive) {
2866
- await this.checkPromptCleared(myGeneration);
2941
+ this.scheduleDebouncedClear();
2867
2942
  }
2868
2943
  continue;
2869
2944
  }
@@ -2871,10 +2946,11 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2871
2946
  if (this.generation !== myGeneration || !this.started) return;
2872
2947
  if (!snapshot || !looksLikePromptSnapshot(snapshot)) {
2873
2948
  if (this.approvalUIActive) {
2874
- await this.checkPromptCleared(myGeneration);
2949
+ this.scheduleDebouncedClear();
2875
2950
  }
2876
2951
  continue;
2877
2952
  }
2953
+ this.cancelClearDebounce();
2878
2954
  const snapshotHash = hashPromptSnapshot(snapshot);
2879
2955
  if (snapshotHash === this.lastPromptHash) {
2880
2956
  continue;
@@ -2898,13 +2974,31 @@ var TmuxPaneObserver = class _TmuxPaneObserver extends import_events2.EventEmitt
2898
2974
  }
2899
2975
  }
2900
2976
  /** Called when we suspect the approval UI may have vanished. Does a
2901
- * fresh capture; if the pane no longer shows approval UI, fires
2902
- * 'prompt-cleared'. Generation-checked to suppress emit after stop(). */
2903
- 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) {
2904
2982
  try {
2905
2983
  const snapshot = await this.captureSnapshot();
2906
2984
  if (this.generation !== myGeneration || !this.started) return;
2907
- 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)) {
2908
3002
  this.approvalUIActive = false;
2909
3003
  this.lastPromptHash = null;
2910
3004
  this.currentApprovalHeader = null;
@@ -3462,8 +3556,9 @@ function looksLikePromptSnapshot(snapshot) {
3462
3556
  const allLines = stripped.trimEnd().split("\n");
3463
3557
  const fenceMask = computeFenceMask(allLines);
3464
3558
  const windowStart = Math.max(0, allLines.length - 30);
3559
+ const questionWindowStart = Math.max(0, allLines.length - 80);
3465
3560
  let headerIdx = -1;
3466
- for (let i = allLines.length - 1; i >= windowStart; i--) {
3561
+ for (let i = allLines.length - 1; i >= questionWindowStart; i--) {
3467
3562
  if (!fenceMask[i] && isQuestionHeaderLine(allLines[i])) {
3468
3563
  headerIdx = i;
3469
3564
  break;
@@ -3471,9 +3566,16 @@ function looksLikePromptSnapshot(snapshot) {
3471
3566
  }
3472
3567
  if (headerIdx < 0) return false;
3473
3568
  const headerLine = allLines[headerIdx].trim();
3569
+ const isTallHeader = headerIdx < windowStart;
3474
3570
  const optionIndices = [];
3475
3571
  for (let i = headerIdx + 1; i < allLines.length; i++) {
3476
- if (!fenceMask[i] && /^[>\s❯]*[1-9][.)]\s+\S/.test(allLines[i].trim())) {
3572
+ if (fenceMask[i]) continue;
3573
+ const trimmed = allLines[i].trim();
3574
+ if (!trimmed) continue;
3575
+ if ((isTallHeader || optionIndices.length > 0) && isResumedTurnSignal(trimmed) || isComposerBoxAt(allLines, i) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
3576
+ return false;
3577
+ }
3578
+ if (/^(?:[>\s❯]{0,4})[1-9][.)]\s+\S/.test(allLines[i])) {
3477
3579
  optionIndices.push(i);
3478
3580
  }
3479
3581
  }
@@ -3497,7 +3599,7 @@ function looksLikePromptSnapshot(snapshot) {
3497
3599
  }
3498
3600
  }
3499
3601
  }
3500
- if (!hasQuestionFooter) return false;
3602
+ if (!hasQuestionFooter || footerIdx < windowStart) return false;
3501
3603
  if (hasTrailingAgyComposer(allLines, footerIdx, fenceMask)) {
3502
3604
  return false;
3503
3605
  }
@@ -4216,6 +4318,38 @@ function detectActivePromptKind(snapshot) {
4216
4318
  }
4217
4319
  if (lastApprovalLineIdx >= 0 && lastQuestionLineIdx >= 0 && lastFileAccessLineIdx >= 0 && lastSystemErrorLineIdx >= 0) break;
4218
4320
  }
4321
+ if (lastQuestionLineIdx < 0 && lastApprovalLineIdx < 0 && lastFileAccessLineIdx < 0) {
4322
+ const tallQuestionStart = Math.max(0, lines.length - 80);
4323
+ for (let i = windowStart - 1; i >= tallQuestionStart; i--) {
4324
+ if (lineInFence[i]) continue;
4325
+ if (isQuestionHeaderLine(lines[i])) {
4326
+ let optCount = 0;
4327
+ let footerInBottomWindow = false;
4328
+ let footerIdx = i;
4329
+ let interrupted = false;
4330
+ for (let j = i + 1; j < lines.length; j++) {
4331
+ if (lineInFence[j]) continue;
4332
+ const trimmed = lines[j].trim();
4333
+ if (!trimmed) continue;
4334
+ if (approvalRe.test(trimmed) || fileEditQuestionRe.test(trimmed) || fileAccessRe.test(trimmed) || fileAccessQuestionRe.test(trimmed) || /Do you want to proceed\?/i.test(trimmed) || isResumedTurnSignal(trimmed) || isComposerBoxAt(lines, j) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
4335
+ interrupted = true;
4336
+ break;
4337
+ }
4338
+ if (/^(?:[>\s❯]{0,4})[1-9][.)]\s+\S/.test(lines[j])) {
4339
+ optCount += 1;
4340
+ } else if (isQuestionFooterLine(trimmed) && j >= windowStart) {
4341
+ footerInBottomWindow = true;
4342
+ footerIdx = j;
4343
+ break;
4344
+ }
4345
+ }
4346
+ if (!interrupted && optCount >= 2 && footerInBottomWindow && !hasTrailingAgyComposer(lines, footerIdx, lineInFence)) {
4347
+ lastQuestionLineIdx = i;
4348
+ }
4349
+ break;
4350
+ }
4351
+ }
4352
+ }
4219
4353
  if (lastApprovalLineIdx < 0 && lastQuestionLineIdx < 0) {
4220
4354
  for (let i = lines.length - 1; i >= windowStart; i--) {
4221
4355
  if (/Do you want to proceed\?/i.test(lines[i])) {
@@ -4241,7 +4375,7 @@ function detectActivePromptKind(snapshot) {
4241
4375
  for (let j = lastSystemErrorLineIdx + 1; j < lines.length; j++) {
4242
4376
  if (!lineInFence[j]) {
4243
4377
  const trimmed = lines[j].trim();
4244
- if (hasQuestionOptions && isResumedTurnSignal(trimmed)) {
4378
+ if ((hasQuestionOptions || lastQuestionLineIdx < windowStart) && isResumedTurnSignal(trimmed)) {
4245
4379
  break;
4246
4380
  }
4247
4381
  if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
@@ -4295,7 +4429,7 @@ function detectActivePromptKind(snapshot) {
4295
4429
  if (isAgyStatusFooterLine(trimmed) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(trimmed) || isComposerBoxAt(lines, j)) {
4296
4430
  break;
4297
4431
  }
4298
- if (hasFoundOptions && isResumedTurnSignal(trimmed)) {
4432
+ if ((hasFoundOptions || lastQuestionLineIdx < windowStart) && isResumedTurnSignal(trimmed)) {
4299
4433
  break;
4300
4434
  }
4301
4435
  if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
@@ -4701,8 +4835,16 @@ function extractQuestionHeader(snapshot) {
4701
4835
  }
4702
4836
  function hashPromptSnapshot(snapshot) {
4703
4837
  let normalized = stripAnsiAndExpandRep(snapshot).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
4704
- if (detectActivePromptKind(normalized) === "system_error") {
4838
+ const activeKind = detectActivePromptKind(normalized);
4839
+ if (activeKind === "system_error") {
4705
4840
  normalized = normalized.replace(/\bResets\s+in\s+\d+[dhms](?:\s*\d+[dhms])*\.?/gi, "Resets in <duration>");
4841
+ } else if (activeKind === "approval") {
4842
+ const fileEdit = extractFileEditPrompt(snapshot);
4843
+ if (fileEdit?.body) {
4844
+ normalized = `${normalized}
4845
+ @@file-edit-body@@
4846
+ ${fileEdit.body}`;
4847
+ }
4706
4848
  }
4707
4849
  return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
4708
4850
  }
@@ -4762,7 +4904,7 @@ function parseApprovalSnapshot(snapshot) {
4762
4904
  }
4763
4905
  }
4764
4906
  if (lastFileEditIdx > lastCmdApprovalIdx) {
4765
- const fileEdit = extractFileEditPrompt(stripped);
4907
+ const fileEdit = extractFileEditPrompt(snapshot);
4766
4908
  if (fileEdit) {
4767
4909
  return parseFileEditSnapshot(stripped, snapshot, fileEdit);
4768
4910
  }
@@ -4780,7 +4922,8 @@ function buildApprovalSemanticKey(candidate) {
4780
4922
  body: isDiffBody ? rawBody.trim() : normalize(rawBody),
4781
4923
  options: (candidate.options ?? []).map((option) => ({
4782
4924
  number: option.number,
4783
- text: normalize(option.text)
4925
+ text: normalize(option.text),
4926
+ ...option.rawLines && option.rawLines.length > 1 ? { rawLines: option.rawLines.map((line) => normalize(line)) } : {}
4784
4927
  })),
4785
4928
  submitMap: Object.fromEntries(
4786
4929
  Object.entries(candidate.submitMap ?? {}).sort(([left], [right]) => Number(left) - Number(right)).map(([number, keys]) => [number, [...keys]])
@@ -4788,6 +4931,13 @@ function buildApprovalSemanticKey(candidate) {
4788
4931
  };
4789
4932
  return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
4790
4933
  }
4934
+ function buildApprovalRedrawSemanticKey(candidate) {
4935
+ const rightTrimmedBody = (candidate.body ?? "").replace(/\r\n/g, "\n").split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n");
4936
+ return buildApprovalSemanticKey({
4937
+ ...candidate,
4938
+ body: rightTrimmedBody
4939
+ });
4940
+ }
4791
4941
  function hasNegativeApprovalOption(options) {
4792
4942
  return options.some((option) => /^(?:no|deny|reject|cancel)\b/i.test(option.text.trim()));
4793
4943
  }
@@ -4894,7 +5044,7 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
4894
5044
  }
4895
5045
  }
4896
5046
  const belowHeader = headerIdx >= 0 ? lines.slice(headerIdx + 1).join("\n") : stripped;
4897
- const options = parseOptions(belowHeader);
5047
+ const options = parseOptions(belowHeader, 80);
4898
5048
  if (options.length < 2) return null;
4899
5049
  const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
4900
5050
  const isMultiSelect = /\bselect\s+(?:all|multiple)\b/i.test(normalizedHeader) || /\bselect\s+(?:all|multiple)\b/i.test(belowHeader) || /(?:<space>|\bspace)\s+(?:to\s+)?(?:select|toggle)\b/i.test(belowHeader) || /\bctrl\+a\s+select\s+all\b/i.test(belowHeader);
@@ -4915,7 +5065,7 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
4915
5065
  }
4916
5066
  function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
4917
5067
  const lines = belowHeader.split("\n");
4918
- const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
5068
+ const optionRegex = /^(?:[>\s❯]{0,4})([1-9])[.)]\s+(.+?)\s*$/;
4919
5069
  const lineInFence = computeFenceMask(lines);
4920
5070
  let firstOptionIdx = -1;
4921
5071
  for (let i = 0; i < lines.length; i++) {
@@ -4928,7 +5078,34 @@ function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
4928
5078
  if (firstOptionIdx <= 0) return "";
4929
5079
  return lines.slice(0, firstOptionIdx).join("\n").trim();
4930
5080
  }
4931
- function parseOptions(snapshot) {
5081
+ function isOptionContinuationBoundary(trimmed, rawLine, nextTrimmedLine) {
5082
+ if (!trimmed) return true;
5083
+ if (/^[•‣◦▪▫]\s/.test(trimmed)) return true;
5084
+ if (/^\s{2,4}[-*]\s/.test(rawLine)) return true;
5085
+ if (isQuestionFooterLine(trimmed)) return true;
5086
+ if (isQuestionHeaderLine(trimmed)) return true;
5087
+ if (isResumedTurnSignal(trimmed)) return true;
5088
+ if (/^[─━═-]{4,}$/.test(trimmed)) return true;
5089
+ if (/^\s*esc to cancel\b/i.test(trimmed)) return true;
5090
+ if (/^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate\b/i.test(trimmed)) return true;
5091
+ if (/^ctrl\+[a-z0-9]+\s+[a-zA-Z]+$/i.test(trimmed)) return true;
5092
+ if (/^\(?use\s+(?:arrow|\S+)\s+keys[^)]*\)?$/i.test(trimmed)) return true;
5093
+ if (!/^ {5,}/.test(rawLine) && /^Your answer:/i.test(trimmed) && (/^Your answer:\s*$/i.test(trimmed) || Boolean(
5094
+ nextTrimmedLine && (isQuestionFooterLine(nextTrimmedLine) || /^\(?Enter to submit(?:\s*,\s*esc to cancel)?\)?\s*$/i.test(nextTrimmedLine) || /^\s*esc to cancel\b/i.test(nextTrimmedLine))
5095
+ ))) {
5096
+ return true;
5097
+ }
5098
+ if (!/^ {5,}/.test(rawLine) && /^\(?Enter to submit(?:\s*,\s*esc to cancel)?\)?\s*$/i.test(trimmed)) {
5099
+ return true;
5100
+ }
5101
+ if (/^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) return true;
5102
+ if (/Requesting permission for:/i.test(trimmed)) return true;
5103
+ if (/Do you want to proceed\?/i.test(trimmed)) return true;
5104
+ if (/^\s*Allow access to this file\?\s*$/i.test(trimmed)) return true;
5105
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(trimmed)) return true;
5106
+ return false;
5107
+ }
5108
+ function parseOptions(snapshot, maxRecentLines = 30) {
4932
5109
  const lines = snapshot.split("\n");
4933
5110
  const lineInFence = computeFenceMask(lines);
4934
5111
  const filteredLines = [];
@@ -4940,16 +5117,72 @@ function parseOptions(snapshot) {
4940
5117
  while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === "") {
4941
5118
  filteredLines.pop();
4942
5119
  }
4943
- const recent = filteredLines.slice(-30);
4944
- const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
5120
+ const recent = filteredLines.slice(-Math.max(30, maxRecentLines));
5121
+ const optionRegex = /^(?:[>\s❯]{0,4})([1-9])[.)]\s+(.+?)\s*$/;
4945
5122
  const found = /* @__PURE__ */ new Map();
4946
- for (const line of recent) {
5123
+ let currentOptionNum = null;
5124
+ let currentOptionTextCol = 5;
5125
+ let prevOptionRawLen = 0;
5126
+ for (let idx = 0; idx < recent.length; idx++) {
5127
+ const line = recent[idx];
4947
5128
  const m = line.match(optionRegex);
4948
- if (!m) continue;
4949
- const num = m[1];
4950
- found.set(num, m[2].trim());
5129
+ if (m) {
5130
+ const num = m[1];
5131
+ const text = m[2].trim();
5132
+ const prefixMatch = line.match(/^(?:[>\s❯]{0,4})[1-9][.)]\s+/);
5133
+ currentOptionTextCol = prefixMatch ? prefixMatch[0].length : 5;
5134
+ prevOptionRawLen = line.trimEnd().length;
5135
+ found.set(num, {
5136
+ text,
5137
+ rawLines: [text],
5138
+ rawLineLengths: [prevOptionRawLen]
5139
+ });
5140
+ currentOptionNum = /^\s*write-?in(?:\s*(?:\.{3}|…))?\s*$/i.test(text) ? null : num;
5141
+ continue;
5142
+ }
5143
+ const trimmed = line.trim();
5144
+ if (currentOptionNum !== null) {
5145
+ let nextTrimmedLine;
5146
+ for (let k = idx + 1; k < recent.length; k++) {
5147
+ const nt = recent[k].trim();
5148
+ if (nt) {
5149
+ nextTrimmedLine = nt;
5150
+ break;
5151
+ }
5152
+ }
5153
+ const leadingSpaces = (line.match(/^\s*/)?.[0] ?? "").length;
5154
+ const isIndentedContinuation = leadingSpaces >= Math.max(4, currentOptionTextCol);
5155
+ const isColumnZeroWrapContinuation = leadingSpaces === 0;
5156
+ if (isOptionContinuationBoundary(trimmed, line, nextTrimmedLine) || !isIndentedContinuation && !isColumnZeroWrapContinuation) {
5157
+ currentOptionNum = null;
5158
+ } else {
5159
+ const entry = found.get(currentOptionNum);
5160
+ if (entry) {
5161
+ const existing = entry.text;
5162
+ entry.text = existing ? `${existing} ${trimmed}` : trimmed;
5163
+ entry.rawLines.push(trimmed);
5164
+ prevOptionRawLen = line.trimEnd().length;
5165
+ entry.rawLineLengths.push(prevOptionRawLen);
5166
+ }
5167
+ }
5168
+ }
4951
5169
  }
4952
- return Array.from(found.entries()).sort(([a], [b]) => Number(a) - Number(b)).map(([number, text]) => ({ number, text }));
5170
+ return Array.from(found.entries()).sort(([a], [b]) => Number(a) - Number(b)).map(([number, entry]) => {
5171
+ const opt = { number, text: entry.text };
5172
+ if (entry.rawLines.length > 1) {
5173
+ Object.defineProperty(opt, "rawLines", {
5174
+ value: entry.rawLines,
5175
+ enumerable: false,
5176
+ configurable: true
5177
+ });
5178
+ Object.defineProperty(opt, "rawLineLengths", {
5179
+ value: entry.rawLineLengths,
5180
+ enumerable: false,
5181
+ configurable: true
5182
+ });
5183
+ }
5184
+ return opt;
5185
+ });
4953
5186
  }
4954
5187
  function buildApprovalSubmitMap(options) {
4955
5188
  const map = {};
@@ -4970,10 +5203,11 @@ function isWriteInInputActive(paneText, expectedHeader, expectedStep, expectedBo
4970
5203
  if (detectActivePromptKind(paneText) !== "question") return false;
4971
5204
  const stripped = stripAnsiAndExpandRep(paneText);
4972
5205
  const lines = stripped.trimEnd().split("\n");
4973
- const windowStart = Math.max(0, lines.length - 30);
5206
+ const fenceMask = computeFenceMask(lines);
5207
+ const windowStart = Math.max(0, lines.length - 80);
4974
5208
  let headerIdx = -1;
4975
5209
  for (let i = lines.length - 1; i >= windowStart; i--) {
4976
- if (isQuestionHeaderLine(lines[i])) {
5210
+ if (!fenceMask[i] && isQuestionHeaderLine(lines[i])) {
4977
5211
  headerIdx = i;
4978
5212
  break;
4979
5213
  }
@@ -5763,6 +5997,32 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5763
5997
  });
5764
5998
  return null;
5765
5999
  }
6000
+ if (candidate.kind === "approval") {
6001
+ const candidateSemanticKey = buildApprovalSemanticKey(candidate);
6002
+ const candidateRedrawSemanticKey = buildApprovalRedrawSemanticKey(candidate);
6003
+ const existing = [...this.pendingPrompts.values()].find(
6004
+ (p) => p.conversationId === conversationId && p.kind === candidate.kind && (p.paneSemanticKey === candidateSemanticKey || p.paneRedrawSemanticKey !== void 0 && p.paneRedrawSemanticKey === candidateRedrawSemanticKey)
6005
+ );
6006
+ if (existing) {
6007
+ if (existing.paneHash && existing.paneHash !== candidate.paneHash) {
6008
+ this.paneOnlyEmittedHashes.delete(existing.paneHash);
6009
+ }
6010
+ existing.paneHash = candidate.paneHash;
6011
+ if (candidate.paneHash) {
6012
+ this.paneOnlyEmittedHashes.add(candidate.paneHash);
6013
+ }
6014
+ existing.paneSemanticKey = candidateSemanticKey;
6015
+ existing.paneRedrawSemanticKey = candidateRedrawSemanticKey;
6016
+ existing.body = candidate.body;
6017
+ logger.debug("Skipping pane-only re-emit for active approval with matching semantic key", {
6018
+ promptId: existing.promptId,
6019
+ paneSemanticKey: candidateSemanticKey.substring(0, 16),
6020
+ paneRedrawSemanticKey: candidateRedrawSemanticKey.substring(0, 16),
6021
+ paneHash: candidate.paneHash.substring(0, 16)
6022
+ });
6023
+ return null;
6024
+ }
6025
+ }
5766
6026
  if (candidate.kind === "question") {
5767
6027
  const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
5768
6028
  const candidateSemanticKey = buildApprovalSemanticKey(candidate);
@@ -5852,10 +6112,13 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5852
6112
  this.emit("prompt-upgraded", existing);
5853
6113
  return existing;
5854
6114
  }
6115
+ const hasPriorPaneObservation = Boolean(existing.paneSemanticKey);
5855
6116
  const areControlsMutated = (existingOpts, candidateOpts) => {
5856
6117
  if (!existingOpts || !candidateOpts) return false;
5857
6118
  if (existingOpts.length !== candidateOpts.length) return true;
5858
- return !existingOpts.every((o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim());
6119
+ return !existingOpts.every(
6120
+ (o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim() && (!hasPriorPaneObservation || JSON.stringify(o.rawLines ?? []) === JSON.stringify(candidateOpts[idx].rawLines ?? []))
6121
+ );
5859
6122
  };
5860
6123
  const isControlDelta = Boolean(
5861
6124
  existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
@@ -5871,6 +6134,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5871
6134
  } else {
5872
6135
  if (!existing.paneSemanticKey) {
5873
6136
  existing.paneSemanticKey = candidateSemanticKey;
6137
+ if (candidate.options && candidate.options.length > 0) {
6138
+ existing.paneOptions = candidate.options;
6139
+ }
5874
6140
  }
5875
6141
  if (stepLineMatch) {
5876
6142
  existing.matchedPaneHeader = stepLineMatch.stitchedHeaderText;
@@ -5999,6 +6265,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5999
6265
  body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
6000
6266
  paneOptions: candidate.options,
6001
6267
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6268
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6002
6269
  kind: candidate.kind,
6003
6270
  isHookPrompt: isHookCall,
6004
6271
  // Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
@@ -6319,6 +6586,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6319
6586
  body: candidate.body,
6320
6587
  paneOptions: candidate.options,
6321
6588
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6589
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6322
6590
  kind: candidate.kind,
6323
6591
  emittedAt: Date.now(),
6324
6592
  ttlMs: this.promptTtlMs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.101",
3
+ "version": "2.0.103",
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": {