antigravity-mobile-proxy 0.1.6 → 0.1.7

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 (113) hide show
  1. package/.next/standalone/.next/app-path-routes-manifest.json +2 -0
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/routes-manifest.json +12 -0
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  9. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  10. package/.next/standalone/.next/server/app/_not-found/page.js +5 -6
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/api/v1/artifacts/active/route.js +5 -1
  14. package/.next/standalone/.next/server/app/api/v1/artifacts/active/route.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/api/v1/changes/active/route/app-paths-manifest.json +3 -0
  16. package/.next/standalone/.next/server/app/api/v1/changes/active/route/build-manifest.json +11 -0
  17. package/.next/standalone/.next/server/app/api/v1/changes/active/route/server-reference-manifest.json +4 -0
  18. package/.next/standalone/.next/server/app/api/v1/changes/active/route.js +11 -0
  19. package/.next/standalone/.next/server/app/api/v1/changes/active/route.js.map +5 -0
  20. package/.next/standalone/.next/server/app/api/v1/changes/active/route.js.nft.json +1 -0
  21. package/.next/standalone/.next/server/app/api/v1/changes/active/route_client-reference-manifest.js +2 -0
  22. package/.next/standalone/.next/server/app/api/v1/changes/diff/route/app-paths-manifest.json +3 -0
  23. package/.next/standalone/.next/server/app/api/v1/changes/diff/route/build-manifest.json +11 -0
  24. package/.next/standalone/.next/server/app/api/v1/changes/diff/route/server-reference-manifest.json +4 -0
  25. package/.next/standalone/.next/server/app/api/v1/changes/diff/route.js +7 -0
  26. package/.next/standalone/.next/server/app/api/v1/changes/diff/route.js.map +5 -0
  27. package/.next/standalone/.next/server/app/api/v1/changes/diff/route.js.nft.json +1 -0
  28. package/.next/standalone/.next/server/app/api/v1/changes/diff/route_client-reference-manifest.js +2 -0
  29. package/.next/standalone/.next/server/app/api/v1/chat/stream/route.js +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/chat/stream/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/v1/windows/cdp-start/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/v1/windows/cdp-status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/v1/windows/close/route.js +1 -1
  34. package/.next/standalone/.next/server/app/api/v1/windows/close/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/v1/windows/open/route.js +2 -2
  36. package/.next/standalone/.next/server/app/api/v1/windows/open/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/debug/page/build-manifest.json +3 -3
  38. package/.next/standalone/.next/server/app/debug/page.js +5 -5
  39. package/.next/standalone/.next/server/app/debug/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/debug/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  42. package/.next/standalone/.next/server/app/page.js +5 -5
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app-paths-manifest.json +2 -0
  46. package/.next/standalone/.next/server/chunks/[root-of-the-server]__26662154._.js +1 -1
  47. package/.next/standalone/.next/server/chunks/[root-of-the-server]__53c4f34d._.js +1 -1
  48. package/.next/standalone/.next/server/chunks/[root-of-the-server]__851f6b5a._.js +3 -0
  49. package/.next/standalone/.next/server/chunks/[root-of-the-server]__94275f7f._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/[root-of-the-server]__9a1969e6._.js +3 -0
  51. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c34d50c8._.js +1 -1
  52. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c696771d._.js +1 -1
  53. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d13bbe3c._.js +3 -0
  54. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d172e6aa._.js +3 -0
  55. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_v1_changes_active_route_actions_1bb9fc18.js +3 -0
  56. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_v1_changes_diff_route_actions_65d9ee16.js +3 -0
  57. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__d080cb3d._.js → [root-of-the-server]__012405ac._.js} +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__a457c799._.js +3 -0
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__f62d412e._.js → [root-of-the-server]__b9356576._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__52af585c._.js → [root-of-the-server]__ce78239f._.js} +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__ae6d24d9._.js → [root-of-the-server]__f47dc36d._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/{_524b2348._.js → _657ecbe9._.js} +3 -3
  63. package/.next/standalone/.next/server/chunks/ssr/{_fe4475aa._.js → _939145a4._.js} +3 -3
  64. package/.next/standalone/.next/server/chunks/ssr/app_layout_tsx_271801d7._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/app_not-found_tsx_ef35050a._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/components_chat-container_tsx_fcbc457f._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_ece394eb.js +3 -0
  68. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_39f173ba.js → node_modules_next_dist_esm_build_templates_app-page_7f45f9bf.js} +3 -3
  69. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_f183c70b._.js → node_modules_next_dist_f21d913a._.js} +2 -2
  70. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  71. package/.next/standalone/.next/server/pages/500.html +2 -2
  72. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/.next/standalone/app/api/v1/artifacts/active/[filename]/route.ts +73 -23
  75. package/.next/standalone/app/api/v1/artifacts/active/route.ts +103 -52
  76. package/.next/standalone/app/api/v1/changes/active/route.ts +27 -0
  77. package/.next/standalone/app/api/v1/changes/diff/route.ts +119 -0
  78. package/.next/standalone/app/globals.css +424 -0
  79. package/.next/standalone/app/layout.tsx +3 -3
  80. package/.next/standalone/app/not-found.tsx +14 -23
  81. package/.next/standalone/components/artifact-panel.tsx +57 -13
  82. package/.next/standalone/components/changes-panel.tsx +178 -0
  83. package/.next/standalone/components/chat-container.tsx +44 -3
  84. package/.next/standalone/components/chat-input.tsx +44 -0
  85. package/.next/standalone/components/header.tsx +1 -13
  86. package/.next/standalone/hooks/use-changes.ts +61 -0
  87. package/.next/standalone/hooks/use-chat.ts +21 -3
  88. package/.next/standalone/hooks/use-conversations.ts +19 -11
  89. package/.next/standalone/lib/scraper/agent-state.ts +89 -54
  90. package/.next/standalone/lib/scraper/chat-history.ts +215 -85
  91. package/.next/standalone/lib/scraper/ide-artifacts.ts +212 -0
  92. package/.next/standalone/lib/scraper/ide-changes.ts +172 -0
  93. package/.next/standalone/lib/types.ts +11 -0
  94. package/.next/standalone/package-lock.json +2 -2
  95. package/.next/standalone/package.json +2 -2
  96. package/.next/standalone/scripts/check-send-button.js +126 -0
  97. package/.next/standalone/scripts/find-send-btn.js +65 -0
  98. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  99. package/.next/static/chunks/09dc2aa5c698c324.css +1 -0
  100. package/.next/static/chunks/{f7c83373e6561461.js → b9a0fabf54a78ef2.js} +1 -1
  101. package/.next/static/chunks/e2ccf5908cad5a88.js +5 -0
  102. package/.next/static/chunks/f7cc8fe5822bbc01.js +1 -0
  103. package/.next/static/chunks/{turbopack-3f34081d758747ed.js → turbopack-7b5dc393c5d3964b.js} +1 -1
  104. package/package.json +2 -2
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__151eca3a._.js +0 -3
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ec32b318._.js +0 -3
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f77eb371._.js +0 -3
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_9170b7a0._.js +0 -3
  109. package/.next/standalone/app/global-error.tsx +0 -42
  110. package/.next/static/chunks/2317ab948a7d90a4.js +0 -5
  111. package/.next/static/chunks/2d277a81099566c3.js +0 -1
  112. package/.next/static/chunks/ad1121f40e497811.css +0 -1
  113. package/.next/static/chunks/d5d4abede4bc89fd.js +0 -1
@@ -80,68 +80,103 @@ export async function getFullAgentState(ctx: ProxyContext): Promise<AgentState>
80
80
  const wrapper = inputArea.closest('.flex') || inputArea.parentElement?.parentElement || inputArea.parentElement;
81
81
  if (wrapper) {
82
82
  (window as any).__proxyInputBoxHTML = wrapper.outerHTML;
83
- const inputBtns = wrapper.querySelectorAll('button');
84
83
 
85
- let hasStop = false;
86
- let hasSend = false;
84
+ // ── Priority check: look for the send/cancel element by data-tooltip-id.
85
+ // In current Antigravity versions, the send/cancel control is a <div>,
86
+ // NOT a <button>, with tooltip like "input-send-button-cancel-tooltip"
87
+ // or "input-send-button-tooltip".
88
+ const sendCancelEl = wrapper.querySelector('[data-tooltip-id*="send"]') ||
89
+ wrapper.querySelector('[data-tooltip-id*="cancel"]');
87
90
 
88
- for (const btn of inputBtns) {
89
- const html = btn.innerHTML || '';
90
- const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
91
- const text = (btn.textContent || '').trim().toLowerCase();
92
- const tooltipId = (btn.getAttribute('data-tooltip-id') || '').toLowerCase();
91
+ if (sendCancelEl) {
92
+ const tooltipId = (sendCancelEl.getAttribute('data-tooltip-id') || '').toLowerCase();
93
+ const innerHtml = sendCancelEl.innerHTML || '';
93
94
 
94
- // The send/stop button changes its tooltip and contents.
95
- // We must be careful not to match 'lucide-square-slash' or other derived
96
- // square icons by ensuring 'lucide-square' is followed by a non-word character.
97
- const isStopIcon =
98
- html.match(/lucide-square(?:[^a-z0-9-]|$)/i) ||
99
- html.includes('lucide-circle-stop') ||
100
- html.includes('lucide-octagon');
95
+ // When running: tooltip contains "cancel", inner HTML has a red/colored
96
+ // square (stop icon) like <div class="bg-red-500 ...rounded-xs">
97
+ const isCancelMode = tooltipId.includes('cancel') ||
98
+ innerHtml.includes('bg-red') ||
99
+ innerHtml.includes('rounded-xs') ||
100
+ innerHtml.match(/lucide-square(?:[^a-z0-9-]|$)/i);
101
+
102
+ // When idle: tooltip is just "send" without "cancel", inner HTML has
103
+ // an arrow-up or send icon
104
+ const isSendMode = !isCancelMode && (
105
+ tooltipId.includes('send') ||
106
+ innerHtml.includes('lucide-arrow-up') ||
107
+ innerHtml.includes('lucide-send')
108
+ );
109
+
110
+ if (isCancelMode) {
111
+ isRunning = true;
112
+ buttonStateDefinitive = true;
113
+ } else if (isSendMode) {
114
+ isRunning = false;
115
+ buttonStateDefinitive = true;
116
+ }
117
+ }
101
118
 
102
- if (
103
- isStopIcon ||
104
- ariaLabel.includes('stop') ||
105
- ariaLabel.includes('cancel') ||
106
- text === 'stop' ||
107
- tooltipId.includes('stop')
108
- ) {
109
- hasStop = true;
119
+ // ── Fallback: scan <button> elements in the wrapper
120
+ if (!buttonStateDefinitive) {
121
+ const inputBtns = wrapper.querySelectorAll('button');
122
+
123
+ let hasStop = false;
124
+ let hasSend = false;
125
+
126
+ for (const btn of inputBtns) {
127
+ const html = btn.innerHTML || '';
128
+ const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
129
+ const text = (btn.textContent || '').trim().toLowerCase();
130
+ const tooltipId = (btn.getAttribute('data-tooltip-id') || '').toLowerCase();
131
+
132
+ const isStopIcon =
133
+ html.match(/lucide-square(?:[^a-z0-9-]|$)/i) ||
134
+ html.includes('lucide-circle-stop') ||
135
+ html.includes('lucide-octagon');
136
+
137
+ if (
138
+ isStopIcon ||
139
+ ariaLabel.includes('stop') ||
140
+ ariaLabel.includes('cancel') ||
141
+ text === 'stop' ||
142
+ tooltipId.includes('stop')
143
+ ) {
144
+ hasStop = true;
145
+ }
146
+
147
+ if (
148
+ html.includes('lucide-send') ||
149
+ html.includes('lucide-arrow-up') ||
150
+ html.includes('lucide-arrow-right') ||
151
+ html.includes('codicon-send') ||
152
+ html.includes('lucide-corner-down-left') ||
153
+ ariaLabel.includes('send') ||
154
+ ariaLabel.includes('submit') ||
155
+ text === 'send' ||
156
+ tooltipId.includes('send')
157
+ ) {
158
+ hasSend = true;
159
+ }
110
160
  }
111
161
 
112
- if (
113
- html.includes('lucide-send') ||
114
- html.includes('lucide-arrow-up') ||
115
- html.includes('lucide-arrow-right') ||
116
- html.includes('codicon-send') ||
117
- html.includes('lucide-corner-down-left') ||
118
- ariaLabel.includes('send') ||
119
- ariaLabel.includes('submit') ||
120
- text === 'send' ||
121
- tooltipId.includes('send')
122
- ) {
123
- hasSend = true;
162
+ if (hasStop) {
163
+ isRunning = true;
164
+ buttonStateDefinitive = true;
165
+ } else if (hasSend) {
166
+ isRunning = false;
167
+ buttonStateDefinitive = true;
168
+ } else {
169
+ // Send button absent — check for div-based send/cancel element
170
+ const altSendEl = inputArea?.querySelector('[data-tooltip-id*="send"]');
171
+ if (altSendEl) {
172
+ const tip = (altSendEl.getAttribute('data-tooltip-id') || '').toLowerCase();
173
+ isRunning = tip.includes('cancel');
174
+ } else {
175
+ isRunning = true;
176
+ }
177
+ buttonStateDefinitive = true;
124
178
  }
125
179
  }
126
-
127
- // Detection logic:
128
- // - If stop button found → definitely running
129
- // - If send button found → definitely idle
130
- // - If NEITHER found (send button removed from DOM) → running
131
- // Antigravity removes the send button entirely while the agent is
132
- // active; no stop icon replaces it. The wrapper drops from 4 to 3
133
- // buttons (Plus, Mode, Mic — no Send).
134
- if (hasStop) {
135
- isRunning = true;
136
- buttonStateDefinitive = true;
137
- } else if (hasSend) {
138
- isRunning = false;
139
- buttonStateDefinitive = true;
140
- } else {
141
- // Send button absent from wrapper — agent is running
142
- isRunning = true;
143
- buttonStateDefinitive = true;
144
- }
145
180
  }
146
181
  }
147
182
 
@@ -1,7 +1,21 @@
1
1
  /**
2
2
  * Chat history scraper.
3
- * Scrolls the Antigravity conversation view to de-virtualize all content,
4
- * then walks the DOM to extract user/agent messages in order.
3
+ * Scrolls the Antigravity conversation view viewport-by-viewport,
4
+ * waiting for content to load at each position, then extracts
5
+ * user/agent messages in document order.
6
+ *
7
+ * Key design decisions:
8
+ * - Viewport-pinned extraction: collect content at each scroll position
9
+ * rather than scrolling everything first, because Antigravity re-virtualizes
10
+ * content that scrolls out of view.
11
+ * - Wait for skeleton resolution: skeleton blocks (.bg-gray-500/10) appear
12
+ * temporarily while content loads; we wait for them to clear.
13
+ * - Clean deduplication: dedup keys are built from cleaned text (without
14
+ * injected <style> content) to avoid false collisions.
15
+ * - User message filtering: excludes CODE tags, tool containers, and
16
+ * notify blocks that incorrectly match .whitespace-pre-wrap.
17
+ * - Agent response categorization: distinguishes between thinking summaries,
18
+ * tool descriptions, notify messages, and final responses.
5
19
  */
6
20
 
7
21
  import type { ProxyContext, ChatHistory } from '../types';
@@ -22,110 +36,226 @@ export async function getChatHistory(ctx: ProxyContext): Promise<ChatHistory> {
22
36
  if (!scrollArea)
23
37
  return { isRunning: false, turnCount: 0, turns: [] };
24
38
 
25
- // Step 1: Scroll to top to force older content to render
26
- scrollArea.scrollTop = 0;
27
- await new Promise((r) => setTimeout(r, 300));
28
-
29
- // Step 2: Incrementally scroll down to de-virtualize all content
30
- const scrollHeight = scrollArea.scrollHeight;
31
- const viewportHeight = scrollArea.clientHeight;
32
- const scrollStep = viewportHeight * 0.8;
33
- let pos = 0;
34
- while (pos < scrollHeight) {
35
- scrollArea.scrollTop = pos;
36
- await new Promise((r) => setTimeout(r, 100));
37
- pos += scrollStep;
38
- }
39
- scrollArea.scrollTop = scrollArea.scrollHeight;
40
- await new Promise((r) => setTimeout(r, 200));
41
-
42
- // Step 3: Walk the DOM to find messages in document order
43
- const turns: { role: 'user' | 'agent'; content: string }[] = [];
44
- const seen = new Set<string>();
45
-
46
39
  const msgList =
47
40
  (scrollArea as Element).querySelector('.mx-auto') || scrollArea;
48
41
 
49
- const candidates: {
50
- el: Element;
51
- role: 'user' | 'agent';
52
- content: string;
53
- }[] = [];
54
-
55
- // Find user messages
56
- const allWhitespace = msgList.querySelectorAll('.whitespace-pre-wrap');
57
- for (const el of allWhitespace) {
58
- const text = el.textContent?.trim();
59
- if (!text) continue;
60
-
61
- let isInsideAgentResponse = false;
62
- let parent = el.parentElement;
63
- while (parent && parent !== msgList) {
64
- const cls = parent.getAttribute('class') || '';
65
- if (
66
- cls.includes('leading-relaxed') &&
67
- cls.includes('select-text')
68
- ) {
69
- isInsideAgentResponse = true;
70
- break;
42
+ // ── Helper: wait for skeleton blocks in current viewport to resolve ──
43
+ async function waitForLoad(maxWait = 3000) {
44
+ const start = Date.now();
45
+ while (Date.now() - start < maxWait) {
46
+ const skeletons = Array.from(msgList.querySelectorAll(
47
+ '.rounded-lg.bg-gray-500\\/10'
48
+ ));
49
+ let visibleSkeletons = 0;
50
+ const panelRect = scrollArea!.getBoundingClientRect();
51
+ for (const sk of skeletons) {
52
+ const rect = sk.getBoundingClientRect();
53
+ const relTop = rect.top - panelRect.top;
54
+ const relBottom = rect.bottom - panelRect.top;
55
+ if (
56
+ relBottom > -50 &&
57
+ relTop < scrollArea!.clientHeight + 50
58
+ ) {
59
+ visibleSkeletons++;
60
+ }
71
61
  }
72
- parent = parent.parentElement;
62
+ if (visibleSkeletons === 0) break;
63
+ await new Promise((r) => setTimeout(r, 100));
73
64
  }
74
- if (isInsideAgentResponse) continue;
75
-
76
- if (el.closest('[data-lexical-editor]')) continue;
77
- if (el.closest('#antigravity\\.agentSidePanelInputBox')) continue;
65
+ }
78
66
 
79
- const key = 'user:' + text.substring(0, 200);
80
- if (seen.has(key)) continue;
81
- seen.add(key);
67
+ // ── Helper: extract clean text from an element (strips <style>) ──
68
+ function getCleanText(el: Element): string {
69
+ const clone = el.cloneNode(true) as Element;
70
+ clone.querySelectorAll('style, script').forEach((n) => n.remove());
71
+ return (clone as HTMLElement).textContent?.trim() || '';
72
+ }
82
73
 
83
- candidates.push({ el, role: 'user', content: text });
74
+ // ── Helper: extract clean HTML from an element ──
75
+ function getCleanHTML(el: Element): string {
76
+ const clone = el.cloneNode(true) as Element;
77
+ clone.querySelectorAll('style, script').forEach((n) => n.remove());
78
+ // Remove Antigravity interactive UI chrome
79
+ clone
80
+ .querySelectorAll(
81
+ 'svg.cursor-pointer, [class*="cursor-pointer"][class*="opacity-70"], button[class*="opacity-70"]'
82
+ )
83
+ .forEach((n) => n.remove());
84
+ return (clone as HTMLElement).innerHTML?.trim() || '';
84
85
  }
85
86
 
86
- // Find agent response blocks
87
- const allResponses = msgList.querySelectorAll(
88
- '.leading-relaxed.select-text'
89
- );
90
- for (const el of allResponses) {
91
- let hidden = false;
87
+ // ── Helper: check if element is visually hidden ──
88
+ function isHidden(el: Element, root: Element): boolean {
92
89
  let ancestor = el.parentElement;
93
90
  let depth = 0;
94
- while (ancestor && ancestor !== msgList && depth < 15) {
91
+ while (ancestor && ancestor !== root && depth < 15) {
95
92
  const cls = ancestor.getAttribute('class') || '';
96
93
  if (cls.includes('max-h-0') || cls.includes('hidden')) {
97
- hidden = true;
98
- break;
94
+ return true;
95
+ }
96
+ // Check inline styles too
97
+ const style = (ancestor as HTMLElement).style;
98
+ if (
99
+ style &&
100
+ (style.display === 'none' || style.maxHeight === '0px')
101
+ ) {
102
+ return true;
103
+ }
104
+ // Check if the element has zero height (collapsed)
105
+ if ((ancestor as HTMLElement).offsetHeight === 0) {
106
+ return true;
99
107
  }
100
108
  ancestor = ancestor.parentElement;
101
109
  depth++;
102
110
  }
103
- if (hidden) continue;
111
+ return false;
112
+ }
104
113
 
105
- const clone = el.cloneNode(true) as Element;
106
- clone.querySelectorAll('style, script').forEach((n) => n.remove());
107
- // Remove Antigravity interactive UI chrome (@ mention SVGs, copy buttons, etc.)
108
- clone.querySelectorAll(
109
- 'svg.cursor-pointer, [class*="cursor-pointer"][class*="opacity-70"], button[class*="opacity-70"]'
110
- ).forEach((n) => n.remove());
111
- const html = (clone as HTMLElement).innerHTML?.trim();
112
- if (!html) continue;
113
-
114
- const key = 'agent:' + el.textContent?.trim().substring(0, 200);
115
- if (seen.has(key)) continue;
116
- seen.add(key);
117
-
118
- candidates.push({ el, role: 'agent', content: html });
114
+ // ── Viewport-by-viewport extraction ──
115
+ // Items are tagged with their absolute Y position at collection time
116
+ // so we can sort chronologically even after elements get re-virtualized.
117
+ const seen = new Set<string>();
118
+ const candidates: {
119
+ absY: number; // scrollTop + element offset — for chronological sorting
120
+ role: 'user' | 'agent';
121
+ content: string;
122
+ contentType?: string;
123
+ }[] = [];
124
+
125
+ function collectAtCurrentPosition() {
126
+ const panelRect = scrollArea!.getBoundingClientRect();
127
+ const currentScroll = scrollArea!.scrollTop;
128
+
129
+ // ── Collect user messages ──
130
+ for (const el of Array.from(msgList.querySelectorAll('.whitespace-pre-wrap'))) {
131
+ const text = el.textContent?.trim();
132
+ if (!text || text.length < 2) continue;
133
+
134
+ // Skip if inside agent response block
135
+ if (el.closest('.leading-relaxed.select-text')) continue;
136
+ // Skip editor / input box
137
+ if (el.closest('[data-lexical-editor]')) continue;
138
+ if (
139
+ el.closest('#antigravity\\.agentSidePanelInputBox')
140
+ )
141
+ continue;
142
+ // Skip CODE tags (inline code references like `file.ts`)
143
+ if (el.tagName === 'CODE') continue;
144
+ // Skip elements inside tool containers
145
+ if (
146
+ el.closest(
147
+ '.flex.flex-col.gap-2.border.rounded-lg'
148
+ )
149
+ )
150
+ continue;
151
+ // Skip elements inside notify containers (these are agent content)
152
+ if (el.closest('.notify-user-container')) continue;
153
+
154
+ // Check if in current viewport
155
+ const rect = el.getBoundingClientRect();
156
+ const relTop = rect.top - panelRect.top;
157
+ if (
158
+ relTop < -100 ||
159
+ relTop > scrollArea!.clientHeight + 100
160
+ )
161
+ continue;
162
+
163
+ const key = 'user:' + text.substring(0, 300);
164
+ if (seen.has(key)) continue;
165
+ seen.add(key);
166
+
167
+ // Absolute Y position = scroll offset + element's position relative to scroll container
168
+ const absY = currentScroll + relTop;
169
+ candidates.push({ absY, role: 'user', content: text });
170
+ }
171
+
172
+ // ── Collect agent response blocks ──
173
+ for (const el of Array.from(msgList.querySelectorAll(
174
+ '.leading-relaxed.select-text'
175
+ ))) {
176
+ if (isHidden(el, msgList)) continue;
177
+
178
+ // Check if in current viewport
179
+ const rect = el.getBoundingClientRect();
180
+ const relTop = rect.top - panelRect.top;
181
+ if (
182
+ relTop < -100 ||
183
+ relTop > scrollArea!.clientHeight + 100
184
+ )
185
+ continue;
186
+
187
+ // Get clean HTML (strips <style>, <script>, UI chrome)
188
+ const html = getCleanHTML(el);
189
+ if (!html) continue;
190
+
191
+ // Build dedup key from CLEAN text (not raw textContent which includes <style>)
192
+ const cleanText = getCleanText(el);
193
+ if (!cleanText) continue;
194
+
195
+ const key = 'agent:' + cleanText.substring(0, 300);
196
+ if (seen.has(key)) continue;
197
+ seen.add(key);
198
+
199
+ // Categorize the response
200
+ const inNotify = !!el.closest('.notify-user-container');
201
+ const parentClass =
202
+ el.parentElement?.getAttribute('class') || '';
203
+ const isThinkingSummary =
204
+ parentClass.includes('font-medium') &&
205
+ parentClass.includes('pb-0');
206
+
207
+ // Skip thinking summaries (these are short descriptions of agent thoughts,
208
+ // not the actual response to the user)
209
+ if (isThinkingSummary) continue;
210
+
211
+ const contentType = inNotify ? 'notify' : 'response';
212
+ const absY = currentScroll + relTop;
213
+
214
+ candidates.push({
215
+ absY,
216
+ role: 'agent',
217
+ content: html,
218
+ contentType,
219
+ });
220
+ }
119
221
  }
120
222
 
121
- // Sort by document position
122
- candidates.sort((a, b) => {
123
- const pos = a.el.compareDocumentPosition(b.el);
124
- if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
125
- if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
126
- return 0;
127
- });
223
+ // ── Scroll through entire conversation, collecting at each stop ──
224
+ const overallStart = Date.now();
225
+ const OVERALL_TIMEOUT = 30000; // 30s max for the entire scroll
226
+
227
+ scrollArea.scrollTop = 0;
228
+ await waitForLoad(2000);
229
+ await new Promise((r) => setTimeout(r, 200));
230
+ collectAtCurrentPosition();
231
+
232
+ const step = scrollArea.clientHeight * 0.8; // 80% viewport step
233
+ let pos = step;
234
+ let scrollSteps = 0;
235
+ const maxScrollSteps = 200; // Safety limit
128
236
 
237
+ while (pos < scrollArea.scrollHeight + scrollArea.clientHeight) {
238
+ if (Date.now() - overallStart > OVERALL_TIMEOUT) break;
239
+ scrollArea.scrollTop = pos;
240
+ await waitForLoad(1500);
241
+ await new Promise((r) => setTimeout(r, 50));
242
+ collectAtCurrentPosition();
243
+ pos += step;
244
+ scrollSteps++;
245
+ if (scrollSteps > maxScrollSteps) break;
246
+ }
247
+
248
+ // Final collection at bottom
249
+ scrollArea.scrollTop = scrollArea.scrollHeight;
250
+ await waitForLoad(1500);
251
+ await new Promise((r) => setTimeout(r, 200));
252
+ collectAtCurrentPosition();
253
+
254
+ // ── Sort by absolute Y position (chronological order) ──
255
+ candidates.sort((a, b) => a.absY - b.absY);
256
+
257
+ // ── Build final turn list ──
258
+ const turns: { role: 'user' | 'agent'; content: string }[] = [];
129
259
  for (const c of candidates) {
130
260
  turns.push({ role: c.role, content: c.content });
131
261
  }