Skip to content

Commit f53b633

Browse files
six-ddcclaude
andcommitted
fix(navigate): ensure content script is ready before returning
Navigate, reload, goBack, goForward, and tabNew now ping the content script after page load to confirm it can receive commands. Previously, commands sent immediately after navigate could time out because the content script hadn't been injected yet — especially on cross-origin navigations in script mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ecaa105 commit f53b633

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

apps/extension/src/entrypoints/content.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export default defineContentScript({
2121
_sender: Browser.runtime.MessageSender,
2222
sendResponse: (response: unknown) => void,
2323
) => {
24+
// Ping handler — background script uses this to verify content script is ready
25+
if (message.type === 'browser-cli-ping') {
26+
sendResponse({ ready: true });
27+
return false;
28+
}
29+
2430
if (message.type !== 'browser-cli-command') return false;
2531

2632
// Validate the command against the schema

apps/extension/src/lib/command-router.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ async function routeCommand(
150150
const { url } = command.params;
151151
assertSafeUrl(url);
152152
await browser.tabs.update(targetTabId, { url });
153-
// Wait for navigation to complete
153+
// Wait for navigation to complete and content script to be ready
154154
await waitForTabLoad(targetTabId);
155+
await waitForContentScriptReady(targetTabId);
155156
const tab = await browser.tabs.get(targetTabId);
156157
return { url: tab.url, title: tab.title };
157158
}
@@ -162,6 +163,7 @@ async function routeCommand(
162163
func: () => history.back(),
163164
});
164165
await waitForUrlChange(targetTabId, beforeBack.url || '');
166+
await waitForContentScriptReady(targetTabId);
165167
const tab = await browser.tabs.get(targetTabId);
166168
return { url: tab.url };
167169
}
@@ -172,12 +174,14 @@ async function routeCommand(
172174
func: () => history.forward(),
173175
});
174176
await waitForUrlChange(targetTabId, beforeFwd.url || '');
177+
await waitForContentScriptReady(targetTabId);
175178
const tab = await browser.tabs.get(targetTabId);
176179
return { url: tab.url };
177180
}
178181
case 'reload': {
179182
await browser.tabs.reload(targetTabId);
180183
await waitForTabLoad(targetTabId);
184+
await waitForContentScriptReady(targetTabId);
181185
const tab = await browser.tabs.get(targetTabId);
182186
return { url: tab.url, title: tab.title };
183187
}
@@ -198,6 +202,10 @@ async function routeCommand(
198202
if (container) {
199203
if (!import.meta.env.FIREFOX) {
200204
const tab = await browser.tabs.create({ url: url || 'about:blank' });
205+
if (url && tab.id) {
206+
await waitForTabLoad(tab.id);
207+
await waitForContentScriptReady(tab.id);
208+
}
201209
const result: Record<string, unknown> = {
202210
tabId: tab.id,
203211
url: tab.url || url || 'about:blank',
@@ -225,6 +233,10 @@ async function routeCommand(
225233
url: url || 'about:blank',
226234
...((cookieStoreId != null ? { cookieStoreId } : {}) as Record<string, unknown>),
227235
} as Browser.tabs.CreateProperties);
236+
if (url && tab.id) {
237+
await waitForTabLoad(tab.id);
238+
await waitForContentScriptReady(tab.id);
239+
}
228240
const result: Record<string, unknown> = {
229241
tabId: tab.id,
230242
url: tab.url || url || 'about:blank',
@@ -1097,6 +1109,38 @@ function waitForUrlChange(tabId: number, previousUrl: string, timeoutMs = 15_000
10971109
});
10981110
}
10991111

1112+
/**
1113+
* Ping the content script to verify it's ready to receive commands.
1114+
* Used after navigation to ensure the new page's content script is injected.
1115+
* Resolves (never rejects) — special pages (chrome://, about:, PDF) may never
1116+
* have a content script, so we don't want to block the command flow.
1117+
*/
1118+
function waitForContentScriptReady(tabId: number, timeoutMs = 5_000): Promise<void> {
1119+
const POLL_INTERVAL = 200;
1120+
return new Promise((resolve) => {
1121+
const deadline = Date.now() + timeoutMs;
1122+
const attempt = () => {
1123+
if (Date.now() > deadline) {
1124+
resolve();
1125+
return;
1126+
}
1127+
browser.tabs
1128+
.sendMessage(tabId, { type: 'browser-cli-ping' }, { frameId: 0 })
1129+
.then((response: unknown) => {
1130+
if (response && (response as { ready?: boolean }).ready) {
1131+
resolve();
1132+
} else {
1133+
setTimeout(attempt, POLL_INTERVAL);
1134+
}
1135+
})
1136+
.catch(() => {
1137+
setTimeout(attempt, POLL_INTERVAL);
1138+
});
1139+
};
1140+
attempt();
1141+
});
1142+
}
1143+
11001144
function waitForTabLoad(tabId: number, timeoutMs = 15_000): Promise<void> {
11011145
return new Promise((resolve) => {
11021146
const timer = setTimeout(() => {

0 commit comments

Comments
 (0)