Skip to content

Commit 3e288f9

Browse files
committed
Add diagnostic logging and broaden Claude DOM selectors
Debug logging shows exactly where the extension fails in the Chrome DevTools console. Claude selectors broadened with multiple fallback strategies for input, send button, and message detection. Made-with: Cursor
1 parent c310629 commit 3e288f9

2 files changed

Lines changed: 171 additions & 66 deletions

File tree

extensions/chrome/content-scripts/claude.js

Lines changed: 113 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,167 @@
11
/**
22
* Claude content script adapter.
33
*
4-
* Claude uses a ProseMirror contenteditable for input and renders
5-
* conversation turns in structured containers.
4+
* Broadened selectors to handle Claude's evolving DOM structure.
65
*/
76

87
(() => {
98
const VENDOR = "Claude";
109

1110
const adapter = {
1211
getInputElement() {
13-
return document.querySelector(
14-
"[contenteditable='true'].ProseMirror, " +
15-
"div[contenteditable='true'][data-placeholder]"
12+
return (
13+
document.querySelector("[contenteditable='true'].ProseMirror") ||
14+
document.querySelector("div[contenteditable='true'][data-placeholder]") ||
15+
document.querySelector("div[contenteditable='true'][role='textbox']") ||
16+
document.querySelector("div.ProseMirror[contenteditable]") ||
17+
document.querySelector("[contenteditable='true'][aria-label*='message' i]") ||
18+
document.querySelector("[contenteditable='true'][aria-label*='chat' i]") ||
19+
document.querySelector("[contenteditable='true'][aria-label*='Reply' i]") ||
20+
document.querySelector("fieldset [contenteditable='true']") ||
21+
document.querySelector("form [contenteditable='true']") ||
22+
document.querySelector("div[contenteditable='true']")
1623
);
1724
},
1825

1926
getInputValue() {
2027
const el = this.getInputElement();
2128
if (!el) return "";
22-
return el.innerText || "";
29+
return el.innerText || el.textContent || "";
2330
},
2431

2532
setInputValue(text) {
2633
const el = this.getInputElement();
2734
if (!el) return;
2835
el.focus();
29-
el.innerText = text;
36+
37+
// Clear existing content
38+
el.innerHTML = "";
39+
// Insert text as a paragraph (ProseMirror expects block elements)
40+
const p = document.createElement("p");
41+
p.textContent = text;
42+
el.appendChild(p);
43+
44+
// Fire all events ProseMirror/React might listen for
3045
el.dispatchEvent(new Event("input", { bubbles: true }));
46+
el.dispatchEvent(new Event("change", { bubbles: true }));
3147
},
3248

3349
getMessages() {
3450
const messages = [];
35-
const turns = document.querySelectorAll(
36-
"[data-testid='human-turn'], [data-testid='ai-turn'], " +
37-
".font-user-message, .font-claude-message"
38-
);
39-
turns.forEach((el) => {
40-
const isUser =
41-
el.matches("[data-testid='human-turn']") ||
42-
el.matches(".font-user-message");
43-
const text = el.innerText?.trim();
44-
if (text) {
45-
messages.push({ role: isUser ? "user" : "assistant", text });
46-
}
47-
});
48-
return messages;
51+
52+
// Try multiple selector strategies
53+
const strategies = [
54+
// Strategy 1: data-testid attributes
55+
() => {
56+
const humans = document.querySelectorAll("[data-testid='human-turn']");
57+
const ais = document.querySelectorAll("[data-testid='ai-turn']");
58+
humans.forEach((el) => messages.push({ role: "user", text: el.innerText?.trim() }));
59+
ais.forEach((el) => messages.push({ role: "assistant", text: el.innerText?.trim() }));
60+
},
61+
// Strategy 2: font-* classes
62+
() => {
63+
document.querySelectorAll(".font-user-message, .font-claude-message").forEach((el) => {
64+
const role = el.classList.contains("font-user-message") ? "user" : "assistant";
65+
messages.push({ role, text: el.innerText?.trim() });
66+
});
67+
},
68+
// Strategy 3: role-based or generic conversation containers
69+
() => {
70+
document.querySelectorAll("[data-is-streaming], [class*='human'], [class*='assistant']").forEach((el) => {
71+
const cls = el.className || "";
72+
const role = cls.includes("human") || cls.includes("user") ? "user" : "assistant";
73+
messages.push({ role, text: el.innerText?.trim() });
74+
});
75+
},
76+
];
77+
78+
for (const strategy of strategies) {
79+
strategy();
80+
if (messages.length > 0) break;
81+
}
82+
83+
return messages.filter((m) => m.text);
4984
},
5085

5186
isNewConversation() {
5287
return this.getMessages().length === 0;
5388
},
5489

5590
triggerSend() {
56-
const btn = document.querySelector(
57-
"button[aria-label='Send Message'], " +
58-
"button[data-testid='send-message']"
59-
);
91+
// Try multiple button selectors
92+
const btn =
93+
document.querySelector("button[aria-label='Send Message']") ||
94+
document.querySelector("button[aria-label='Send message']") ||
95+
document.querySelector("button[aria-label*='Send' i]") ||
96+
document.querySelector("button[data-testid='send-message']") ||
97+
document.querySelector("button[data-testid*='send' i]") ||
98+
document.querySelector("form button[type='submit']") ||
99+
// Last resort: find a button near the input that looks like send
100+
(() => {
101+
const input = this.getInputElement();
102+
if (!input) return null;
103+
const form = input.closest("form, fieldset, [role='form'], div[class*='composer'], div[class*='input']");
104+
if (!form) return null;
105+
const buttons = form.querySelectorAll("button");
106+
return [...buttons].find((b) =>
107+
b.querySelector("svg") && !b.disabled
108+
);
109+
})();
110+
60111
if (btn) {
112+
log("Clicking send button:", btn.getAttribute("aria-label") || btn.className?.slice(0, 40));
61113
btn.click();
62114
return;
63115
}
116+
117+
log("No send button found. Trying Enter keypress on input.");
64118
const el = this.getInputElement();
65119
if (el) {
66120
el.dispatchEvent(new KeyboardEvent("keydown", {
67-
key: "Enter", code: "Enter", keyCode: 13, bubbles: true,
121+
key: "Enter", code: "Enter", keyCode: 13, which: 13,
122+
bubbles: true, cancelable: true,
123+
}));
124+
el.dispatchEvent(new KeyboardEvent("keypress", {
125+
key: "Enter", code: "Enter", keyCode: 13, which: 13,
126+
bubbles: true, cancelable: true,
127+
}));
128+
el.dispatchEvent(new KeyboardEvent("keyup", {
129+
key: "Enter", code: "Enter", keyCode: 13, which: 13,
130+
bubbles: true, cancelable: true,
68131
}));
69132
}
70133
},
71134

72135
hideLastExchange() {
73-
const turns = document.querySelectorAll(
74-
"[data-testid='human-turn'], [data-testid='ai-turn'], " +
75-
".font-user-message, .font-claude-message"
76-
);
77-
const toHide = [...turns].slice(-2);
78-
toHide.forEach((el) => {
79-
const container = el.closest("[class*='turn']") || el.parentElement;
80-
if (container) container.style.display = "none";
81-
});
136+
const allMessages = this.getMessages();
137+
if (allMessages.length < 2) return;
138+
139+
// Find all turn containers and hide the last two
140+
const turnSelectors = [
141+
"[data-testid='human-turn'], [data-testid='ai-turn']",
142+
".font-user-message, .font-claude-message",
143+
"[class*='human'], [class*='assistant']",
144+
];
145+
146+
for (const selector of turnSelectors) {
147+
const turns = document.querySelectorAll(selector);
148+
if (turns.length >= 2) {
149+
const toHide = [...turns].slice(-2);
150+
toHide.forEach((el) => {
151+
const container = el.closest("[class*='turn']") ||
152+
el.closest("[class*='row']") ||
153+
el.parentElement;
154+
if (container) container.style.display = "none";
155+
});
156+
break;
157+
}
158+
}
82159
},
83160

84161
onNewMessage(callback) {
85162
const container =
86163
document.querySelector("[data-testid='conversation']") ||
164+
document.querySelector("[class*='conversation']") ||
87165
document.querySelector("main") ||
88166
document.body;
89167
const observer = new MutationObserver(() => {

extensions/chrome/content-scripts/shared.js

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,20 @@
22
* Shared core for Reflect Memory browser extension.
33
*
44
* Architecture: "Lazy Priming" with selective write-back.
5-
*
6-
* READ: The extension waits for the user to submit their first message,
7-
* searches for relevant memories, and only if matches exist, sends a
8-
* hidden priming message before the user's real message. If nothing
9-
* is relevant, zero interference.
10-
*
11-
* WRITE: Only conversations where priming fired (meaning the topic is
12-
* related to stored context) have their exchanges captured back to
13-
* Reflect Memory. Unrelated conversations are never written.
14-
*
15-
* Each vendor script implements a VendorAdapter:
16-
* getInputElement() - returns the chat textarea/contenteditable
17-
* getInputValue() - reads current user input text
18-
* setInputValue(text) - sets the input text
19-
* getMessages() - returns [{ role, text }] from the DOM
20-
* onNewMessage(cb) - calls cb(messages) on DOM changes
21-
* triggerSend() - clicks the send button programmatically
22-
* isNewConversation() - true if the chat has zero messages
23-
* hideLastExchange() - hides the priming message + response from view
245
*/
256

267
const PRIMING_MARKER = "[[REFLECT_MEMORY_PRIME]]";
278
const PRIMED_KEY = "reflect_memory_primed";
289
const WRITE_ENABLED_KEY = "reflect_memory_write_enabled";
10+
const DEBUG = true;
2911

3012
let isPriming = false;
3113
let lastCapturedCount = 0;
3214

15+
function log(...args) {
16+
if (DEBUG) console.log("[Reflect Memory]", ...args);
17+
}
18+
3319
async function sendToBackground(message) {
3420
return new Promise((resolve) => {
3521
chrome.runtime.sendMessage(message, resolve);
@@ -84,21 +70,21 @@ function enableWrite() {
8470
sessionStorage.setItem(getSessionKey(WRITE_ENABLED_KEY), "true");
8571
}
8672

87-
/**
88-
* Intercepts the user's first message in a new conversation.
89-
* Searches for relevant memories. If found, primes the AI first,
90-
* then sends the user's real message. If not found, sends normally
91-
* and write-back stays disabled for this conversation.
92-
*/
9373
async function interceptFirstMessage(adapter, userMessage) {
94-
if (isPriming || isAlreadyPrimed()) return false;
74+
if (isPriming || isAlreadyPrimed()) {
75+
log("Skipping: already primed or priming in progress");
76+
return false;
77+
}
9578
if (!adapter.isNewConversation()) {
79+
log("Skipping: not a new conversation");
9680
markAsPrimed();
9781
return false;
9882
}
9983

10084
const authCheck = await sendToBackground({ type: "CHECK_AUTH" });
85+
log("Auth check:", authCheck);
10186
if (!authCheck?.authenticated) {
87+
log("Not authenticated. Check your agent key.");
10288
markAsPrimed();
10389
return false;
10490
}
@@ -107,11 +93,14 @@ async function interceptFirstMessage(adapter, userMessage) {
10793

10894
try {
10995
const searchTerm = userMessage.slice(0, 200);
96+
log("Searching memories for:", searchTerm);
11097
const response = await sendToBackground({
11198
type: "SEARCH_MEMORIES",
11299
term: searchTerm,
113100
});
114101

102+
log("Search result:", response?.memories?.length || 0, "memories found");
103+
115104
if (!response?.memories?.length) {
116105
isPriming = false;
117106
markAsPrimed();
@@ -125,20 +114,29 @@ async function interceptFirstMessage(adapter, userMessage) {
125114
return false;
126115
}
127116

117+
log("Setting priming text...");
128118
adapter.setInputValue(primingText);
129-
await new Promise((r) => setTimeout(r, 100));
119+
await new Promise((r) => setTimeout(r, 200));
120+
121+
log("Sending priming message...");
130122
adapter.triggerSend();
131123

124+
log("Waiting for AI response...");
132125
await waitForResponse(adapter);
126+
127+
log("Hiding priming exchange...");
133128
adapter.hideLastExchange();
134129
markAsPrimed();
135130
enableWrite();
136131

132+
log("Sending user's real message:", userMessage.slice(0, 50));
137133
adapter.setInputValue(userMessage);
138-
await new Promise((r) => setTimeout(r, 100));
134+
await new Promise((r) => setTimeout(r, 200));
139135
adapter.triggerSend();
140136

141-
} catch {
137+
log("Priming complete.");
138+
} catch (err) {
139+
log("Error during priming:", err);
142140
adapter.setInputValue(userMessage);
143141
await new Promise((r) => setTimeout(r, 50));
144142
adapter.triggerSend();
@@ -162,6 +160,7 @@ function waitForResponse(adapter) {
162160

163161
if (hasResponse || checks >= maxChecks) {
164162
clearInterval(interval);
163+
log("Response wait done. Checks:", checks, "Messages:", current.length);
165164
setTimeout(resolve, 300);
166165
}
167166
}, 500);
@@ -207,6 +206,7 @@ async function captureAsMemory(messages, vendor) {
207206
`Response: ${lastAI.slice(0, 600)}`,
208207
].join("\n\n");
209208

209+
log("Writing memory:", title);
210210
await sendToBackground({
211211
type: "WRITE_MEMORY",
212212
data: {
@@ -218,14 +218,33 @@ async function captureAsMemory(messages, vendor) {
218218
}
219219

220220
function initVendor(adapter, vendorName) {
221+
log(`Initializing ${vendorName} adapter...`);
221222
let debounceTimer = null;
222223
let sendIntercepted = false;
223224

224225
function hookSendInterception() {
225226
const el = adapter.getInputElement();
226-
if (!el || sendIntercepted) return;
227+
if (!el) {
228+
log("Input element NOT found. Selectors need updating.");
229+
log("Scanning page for contenteditable elements...");
230+
const allEditable = document.querySelectorAll("[contenteditable='true']");
231+
log(`Found ${allEditable.length} contenteditable elements:`);
232+
allEditable.forEach((e, i) => {
233+
log(` [${i}] tag=${e.tagName} class="${e.className.slice(0, 80)}" role=${e.getAttribute("role")} placeholder=${e.getAttribute("data-placeholder") || e.getAttribute("aria-placeholder")}`);
234+
});
235+
const allTextareas = document.querySelectorAll("textarea");
236+
log(`Found ${allTextareas.length} textarea elements:`);
237+
allTextareas.forEach((e, i) => {
238+
log(` [${i}] placeholder="${e.placeholder?.slice(0, 50)}" name=${e.name}`);
239+
});
240+
return;
241+
}
242+
if (sendIntercepted) return;
227243
sendIntercepted = true;
228244

245+
log("Input element found:", el.tagName, el.className?.slice(0, 60));
246+
log("Keydown listener attached. Waiting for first Enter press.");
247+
229248
el.addEventListener("keydown", (e) => {
230249
if (e.key !== "Enter" || e.shiftKey) return;
231250
if (isPriming) return;
@@ -234,11 +253,13 @@ function initVendor(adapter, vendorName) {
234253
const userMessage = adapter.getInputValue()?.trim();
235254
if (!userMessage) return;
236255

256+
log("Enter intercepted. Message:", userMessage.slice(0, 50));
237257
e.preventDefault();
238258
e.stopImmediatePropagation();
239259

240260
interceptFirstMessage(adapter, userMessage).then((handled) => {
241261
if (!handled) {
262+
log("No priming needed. Sending original message.");
242263
adapter.setInputValue(userMessage);
243264
setTimeout(() => adapter.triggerSend(), 50);
244265
}
@@ -254,7 +275,13 @@ function initVendor(adapter, vendorName) {
254275
}
255276
}, 800);
256277

257-
setTimeout(() => clearInterval(waitForInput), 30000);
278+
setTimeout(() => {
279+
clearInterval(waitForInput);
280+
if (!sendIntercepted) {
281+
log("TIMEOUT: Input element never found after 30s. Running diagnostic...");
282+
hookSendInterception();
283+
}
284+
}, 30000);
258285

259286
const bodyObserver = new MutationObserver(() => {
260287
if (!sendIntercepted || !adapter.getInputElement()) {

0 commit comments

Comments
 (0)