Skip to content

Commit f8080ee

Browse files
committed
Bind claimed prompts to source tabs
1 parent 5bbe75e commit f8080ee

3 files changed

Lines changed: 71 additions & 29 deletions

File tree

src/chrome/src/ui/sidepanel.js

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7086,33 +7086,49 @@ async function sendMessage(extraChatParams = {}) {
70867086
delete chatExtraParams.__mode;
70877087
delete chatExtraParams.__onContextMenuClaimRejected;
70887088
const contextMenuClaim = chatExtraParams.contextMenuClaim;
7089-
const requestedSourceGrounding = retryOptions?.sourceGrounding ?? chatExtraParams.sourceGrounding;
7090-
const sourceGrounding = requestedSourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
7091-
? SELECTION_ONLY_SOURCE_GROUNDING
7092-
: null;
7093-
delete chatExtraParams.sourceGrounding;
7094-
if (sourceGrounding) chatExtraParams.sourceGrounding = sourceGrounding;
7095-
await waitForVisibleSidePanelStateRefresh();
7096-
stopListening();
7097-
let text = inputEl.value.trim();
7098-
if (!text) return;
7099-
const submittedText = text;
7100-
const tabId = currentTabId;
71017089
let contextMenuClaimOwned = Boolean(contextMenuClaim?.promptId && contextMenuClaim?.claimantId);
7090+
const claimedContextMenuTabId = contextMenuClaimOwned
7091+
? Number(chatExtraParams.contextMenuClear?.tabId ?? currentTabId)
7092+
: null;
71027093
const releaseOwnedContextMenuClaim = async (
71037094
rejection = { reason: 'panel-hidden', retryAfterMs: 250 },
71047095
) => {
71057096
if (!contextMenuClaimOwned) return;
71067097
contextMenuClaimOwned = false;
71077098
try {
71087099
await sendToBackground('release_context_menu_prompt_claim', {
7109-
tabId,
7100+
tabId: claimedContextMenuTabId,
71107101
promptId: contextMenuClaim.promptId,
71117102
claimantId: contextMenuClaim.claimantId,
71127103
});
71137104
} catch { /* the durable lease still expires if release fails */ }
71147105
onContextMenuClaimRejected?.(rejection);
71157106
};
7107+
const requestedSourceGrounding = retryOptions?.sourceGrounding ?? chatExtraParams.sourceGrounding;
7108+
const sourceGrounding = requestedSourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
7109+
? SELECTION_ONLY_SOURCE_GROUNDING
7110+
: null;
7111+
delete chatExtraParams.sourceGrounding;
7112+
if (sourceGrounding) chatExtraParams.sourceGrounding = sourceGrounding;
7113+
await waitForVisibleSidePanelStateRefresh();
7114+
if (contextMenuClaimOwned
7115+
&& (document.visibilityState === 'hidden'
7116+
|| !sameTabId(currentTabId, claimedContextMenuTabId)
7117+
|| !sameTabId(renderedTabId, claimedContextMenuTabId))) {
7118+
await releaseOwnedContextMenuClaim();
7119+
return false;
7120+
}
7121+
stopListening();
7122+
let text = inputEl.value.trim();
7123+
if (!text) {
7124+
if (contextMenuClaimOwned) {
7125+
await releaseOwnedContextMenuClaim({ reason: 'preflight-empty', retryAfterMs: 1_000 });
7126+
return false;
7127+
}
7128+
return;
7129+
}
7130+
const submittedText = text;
7131+
const tabId = currentTabId;
71167132
if (isConversationClearInProgress(tabId)) {
71177133
await releaseOwnedContextMenuClaim({ reason: 'conversation-clear', retryAfterMs: 1_000 });
71187134
return false;

src/firefox/src/ui/sidepanel.js

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6828,33 +6828,49 @@ async function sendMessage(extraChatParams = {}) {
68286828
delete chatExtraParams.__mode;
68296829
delete chatExtraParams.__onContextMenuClaimRejected;
68306830
const contextMenuClaim = chatExtraParams.contextMenuClaim;
6831-
const requestedSourceGrounding = retryOptions?.sourceGrounding ?? chatExtraParams.sourceGrounding;
6832-
const sourceGrounding = requestedSourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
6833-
? SELECTION_ONLY_SOURCE_GROUNDING
6834-
: null;
6835-
delete chatExtraParams.sourceGrounding;
6836-
if (sourceGrounding) chatExtraParams.sourceGrounding = sourceGrounding;
6837-
await waitForVisibleSidePanelStateRefresh();
6838-
stopListening();
6839-
let text = inputEl.value.trim();
6840-
if (!text) return;
6841-
const submittedText = text;
6842-
const tabId = currentTabId;
68436831
let contextMenuClaimOwned = Boolean(contextMenuClaim?.promptId && contextMenuClaim?.claimantId);
6832+
const claimedContextMenuTabId = contextMenuClaimOwned
6833+
? Number(chatExtraParams.contextMenuClear?.tabId ?? currentTabId)
6834+
: null;
68446835
const releaseOwnedContextMenuClaim = async (
68456836
rejection = { reason: 'panel-hidden', retryAfterMs: 250 },
68466837
) => {
68476838
if (!contextMenuClaimOwned) return;
68486839
contextMenuClaimOwned = false;
68496840
try {
68506841
await sendToBackground('release_context_menu_prompt_claim', {
6851-
tabId,
6842+
tabId: claimedContextMenuTabId,
68526843
promptId: contextMenuClaim.promptId,
68536844
claimantId: contextMenuClaim.claimantId,
68546845
});
68556846
} catch { /* the durable lease still expires if release fails */ }
68566847
onContextMenuClaimRejected?.(rejection);
68576848
};
6849+
const requestedSourceGrounding = retryOptions?.sourceGrounding ?? chatExtraParams.sourceGrounding;
6850+
const sourceGrounding = requestedSourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
6851+
? SELECTION_ONLY_SOURCE_GROUNDING
6852+
: null;
6853+
delete chatExtraParams.sourceGrounding;
6854+
if (sourceGrounding) chatExtraParams.sourceGrounding = sourceGrounding;
6855+
await waitForVisibleSidePanelStateRefresh();
6856+
if (contextMenuClaimOwned
6857+
&& (document.visibilityState === 'hidden'
6858+
|| !sameTabId(currentTabId, claimedContextMenuTabId)
6859+
|| !sameTabId(renderedTabId, claimedContextMenuTabId))) {
6860+
await releaseOwnedContextMenuClaim();
6861+
return false;
6862+
}
6863+
stopListening();
6864+
let text = inputEl.value.trim();
6865+
if (!text) {
6866+
if (contextMenuClaimOwned) {
6867+
await releaseOwnedContextMenuClaim({ reason: 'preflight-empty', retryAfterMs: 1_000 });
6868+
return false;
6869+
}
6870+
return;
6871+
}
6872+
const submittedText = text;
6873+
const tabId = currentTabId;
68586874
if (isConversationClearInProgress(tabId)) {
68596875
await releaseOwnedContextMenuClaim({ reason: 'conversation-clear', retryAfterMs: 1_000 });
68606876
return false;

test/run.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21927,7 +21927,7 @@ test('context-menu ownership and stale-panel persistence guards are wired in bot
2192721927
}
2192821928
assert.match(
2192921929
panel,
21930-
/async function sendMessage\(extraChatParams = \{\}\) \{[\s\S]*?await waitForVisibleSidePanelStateRefresh\(\);\s*stopListening\(\);\s*let text = inputEl\.value\.trim\(\);/,
21930+
/async function sendMessage\(extraChatParams = \{\}\) \{[\s\S]*?await waitForVisibleSidePanelStateRefresh\(\);[\s\S]*?stopListening\(\);\s*let text = inputEl\.value\.trim\(\);/,
2193121931
`${label}: user sends should not capture or append a turn until visibility reconciliation finishes`,
2193221932
);
2193321933
assert.match(
@@ -21987,9 +21987,19 @@ test('context-menu ownership and stale-panel persistence guards are wired in bot
2198721987
);
2198821988
assert.match(
2198921989
panel,
21990-
/let contextMenuClaimOwned = Boolean\(contextMenuClaim\?\.promptId && contextMenuClaim\?\.claimantId\);[\s\S]*?const releaseOwnedContextMenuClaim = async \([\s\S]*?reason: 'panel-hidden', retryAfterMs: 250[\s\S]*?contextMenuClaimOwned = false;[\s\S]*?release_context_menu_prompt_claim[\s\S]*?onContextMenuClaimRejected\?\.\(rejection\);[\s\S]*?contextMenuClaimOwned = renewedClaim\?\.claimed === true;/,
21990+
/let contextMenuClaimOwned = Boolean\(contextMenuClaim\?\.promptId && contextMenuClaim\?\.claimantId\);[\s\S]*?const claimedContextMenuTabId = contextMenuClaimOwned[\s\S]*?chatExtraParams\.contextMenuClear\?\.tabId[\s\S]*?const releaseOwnedContextMenuClaim = async \([\s\S]*?reason: 'panel-hidden', retryAfterMs: 250[\s\S]*?contextMenuClaimOwned = false;[\s\S]*?release_context_menu_prompt_claim[\s\S]*?tabId: claimedContextMenuTabId,[\s\S]*?onContextMenuClaimRejected\?\.\(rejection\);[\s\S]*?contextMenuClaimOwned = renewedClaim\?\.claimed === true;/,
2199121991
`${label}: initial and renewed ownership should share one idempotent release-and-retry path`,
2199221992
);
21993+
assert.match(
21994+
panel,
21995+
/const claimedContextMenuTabId = contextMenuClaimOwned[\s\S]*?await waitForVisibleSidePanelStateRefresh\(\);[\s\S]*?if \(contextMenuClaimOwned[\s\S]*?!sameTabId\(currentTabId, claimedContextMenuTabId\)[\s\S]*?!sameTabId\(renderedTabId, claimedContextMenuTabId\)[\s\S]*?await releaseOwnedContextMenuClaim\(\);[\s\S]*?return false;[\s\S]*?let text = inputEl\.value\.trim\(\);/,
21996+
`${label}: claimed prompts should remain bound to their source tab before reading the refreshed composer`,
21997+
);
21998+
assert.match(
21999+
panel,
22000+
/let text = inputEl\.value\.trim\(\);\s*if \(!text\) \{\s*if \(contextMenuClaimOwned\) \{\s*await releaseOwnedContextMenuClaim\(\{ reason: 'preflight-empty', retryAfterMs: 1_000 \}\);\s*return false;/,
22001+
`${label}: an empty refreshed composer should release and retry an owned prompt`,
22002+
);
2199322003
assert.match(
2199422004
panel,
2199522005
/if \(isProcessing\) \{\s*if \(contextMenuClaimOwned\) \{\s*await releaseOwnedContextMenuClaim\(\{ reason: 'run-active', retryAfterMs: 1_000 \}\);\s*return false;/,
@@ -22004,7 +22014,7 @@ test('context-menu ownership and stale-panel persistence guards are wired in bot
2200422014
const sendMessageBody = panel.slice(sendMessageStart, sendMessageEnd);
2200522015
assert.equal(
2200622016
(sendMessageBody.match(/await releaseOwnedContextMenuClaim\(\);/g) || []).length,
22007-
5,
22017+
6,
2200822018
`${label}: every visibility or preflight-abort exit should release the claim and schedule retry`,
2200922019
);
2201022020
assert.match(

0 commit comments

Comments
 (0)