@quantiya/codevibe-antigravity-plugin 2.0.101 → 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.
Files changed (2) hide show
  1. package/dist/server.js +145 -8
  2. package/package.json +1 -1
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;
@@ -4701,8 +4795,16 @@ function extractQuestionHeader(snapshot) {
4701
4795
  }
4702
4796
  function hashPromptSnapshot(snapshot) {
4703
4797
  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") {
4798
+ const activeKind = detectActivePromptKind(normalized);
4799
+ if (activeKind === "system_error") {
4705
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
+ }
4706
4808
  }
4707
4809
  return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
4708
4810
  }
@@ -4762,7 +4864,7 @@ function parseApprovalSnapshot(snapshot) {
4762
4864
  }
4763
4865
  }
4764
4866
  if (lastFileEditIdx > lastCmdApprovalIdx) {
4765
- const fileEdit = extractFileEditPrompt(stripped);
4867
+ const fileEdit = extractFileEditPrompt(snapshot);
4766
4868
  if (fileEdit) {
4767
4869
  return parseFileEditSnapshot(stripped, snapshot, fileEdit);
4768
4870
  }
@@ -4788,6 +4890,13 @@ function buildApprovalSemanticKey(candidate) {
4788
4890
  };
4789
4891
  return (0, import_crypto2.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
4790
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
+ }
4791
4900
  function hasNegativeApprovalOption(options) {
4792
4901
  return options.some((option) => /^(?:no|deny|reject|cancel)\b/i.test(option.text.trim()));
4793
4902
  }
@@ -5763,6 +5872,32 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5763
5872
  });
5764
5873
  return null;
5765
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
+ }
5766
5901
  if (candidate.kind === "question") {
5767
5902
  const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
5768
5903
  const candidateSemanticKey = buildApprovalSemanticKey(candidate);
@@ -5999,6 +6134,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5999
6134
  body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
6000
6135
  paneOptions: candidate.options,
6001
6136
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6137
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6002
6138
  kind: candidate.kind,
6003
6139
  isHookPrompt: isHookCall,
6004
6140
  // Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
@@ -6319,6 +6455,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6319
6455
  body: candidate.body,
6320
6456
  paneOptions: candidate.options,
6321
6457
  paneSemanticKey: buildApprovalSemanticKey(candidate),
6458
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6322
6459
  kind: candidate.kind,
6323
6460
  emittedAt: Date.now(),
6324
6461
  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.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": {