Skip to content

Commit ce5e841

Browse files
committed
Retry stale tab chat clears
1 parent 2ea8ceb commit ce5e841

5 files changed

Lines changed: 90 additions & 18 deletions

File tree

src/chrome/src/background.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,11 +2682,23 @@ async function handleMessage(msg, sender) {
26822682
claimantId: msg.handoffOwnerId,
26832683
});
26842684

2685-
case 'clear_tab_chat':
2686-
return await tabChatHandoff.clear(msg.tabId || sender.tab?.id, {
2685+
case 'clear_tab_chat': {
2686+
const tabId = msg.tabId || sender.tab?.id;
2687+
const result = await tabChatHandoff.clear(tabId, {
26872688
ownerId: msg.handoffOwnerId,
26882689
handoffGeneration: msg.handoffGeneration,
26892690
});
2691+
if (result?.ok && !result.skipped) {
2692+
chrome.runtime.sendMessage({
2693+
target: 'sidepanel',
2694+
action: 'tab_chat_cleared',
2695+
tabId,
2696+
handoffOwnerId: result.handoffOwnerId,
2697+
handoffGeneration: result.handoffGeneration,
2698+
}).catch(() => {});
2699+
}
2700+
return result;
2701+
}
26902702

26912703
case 'list_scheduled_jobs': {
26922704
const tabId = msg.tabId || sender.tab?.id || null;

src/chrome/src/ui/sidepanel.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,9 +1651,10 @@ function clearCachedTabChat(tabId) {
16511651
tabChats.delete(tabId);
16521652
return enqueueTabChatOperation(tabId, async (numericTabId) => {
16531653
tabChats.delete(numericTabId);
1654-
try {
1655-
let handoffGeneration = tabChatHandoffGenerations.get(numericTabId);
1656-
if (!Number.isFinite(handoffGeneration)) {
1654+
let handoffGeneration = tabChatHandoffGenerations.get(numericTabId);
1655+
let clearResult = null;
1656+
while (true) {
1657+
if (!Number.isFinite(handoffGeneration) || clearResult?.reason === 'stale-handoff') {
16571658
const ownership = await sendToBackground('load_tab_chat', {
16581659
tabId: numericTabId,
16591660
waitForHandoff: true,
@@ -1665,13 +1666,13 @@ function clearCachedTabChat(tabId) {
16651666
tabChatHandoffGenerations.set(numericTabId, handoffGeneration);
16661667
}
16671668
}
1668-
await sendToBackground('clear_tab_chat', {
1669+
clearResult = await sendToBackground('clear_tab_chat', {
16691670
tabId: numericTabId,
16701671
handoffOwnerId: tabChatHandoffOwnerId,
16711672
handoffGeneration,
16721673
});
1673-
} catch (e) { /* ignore */ }
1674-
return { ok: true };
1674+
if (!clearResult?.skipped || clearResult.reason !== 'stale-handoff') return clearResult;
1675+
}
16751676
});
16761677
}
16771678

@@ -2099,7 +2100,10 @@ function drainQueuedComposerMessageForCurrentTab() {
20992100
}
21002101

21012102
async function renderClearedConversationForTab(tabId) {
2102-
await clearCachedTabChat(tabId);
2103+
const clearResult = await clearCachedTabChat(tabId);
2104+
if (!clearResult?.ok || clearResult?.skipped) {
2105+
throw new Error(clearResult?.error || 'Unable to clear tab chat.');
2106+
}
21032107
resetComposerHistoryNavigation(tabId);
21042108
saveInputDraftForTab(tabId, '');
21052109
clearPendingAttachmentsForTab(tabId);
@@ -7617,6 +7621,20 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
76177621
});
76187622
});
76197623

7624+
chrome.runtime.onMessage.addListener((msg) => {
7625+
if (msg?.target !== 'sidepanel'
7626+
|| msg.action !== 'tab_chat_cleared'
7627+
|| msg.handoffOwnerId === tabChatHandoffOwnerId
7628+
|| document.visibilityState === 'hidden'
7629+
|| !sameTabId(currentTabId, msg.tabId)) return;
7630+
tabChats.delete(Number(msg.tabId));
7631+
if (lastVisibleTabChatSnapshot
7632+
&& sameTabId(lastVisibleTabChatSnapshot.tabId, msg.tabId)) {
7633+
lastVisibleTabChatSnapshot = null;
7634+
}
7635+
requestVisibleSidePanelStateRefresh();
7636+
});
7637+
76207638
document.addEventListener('visibilitychange', () => {
76217639
if (document.visibilityState === 'hidden') {
76227640
visibleStateRefreshPending = false;

src/firefox/src/background.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,11 +2369,23 @@ async function handleMessage(msg, sender) {
23692369
claimantId: msg.handoffOwnerId,
23702370
});
23712371

2372-
case 'clear_tab_chat':
2373-
return await tabChatHandoff.clear(msg.tabId || sender.tab?.id, {
2372+
case 'clear_tab_chat': {
2373+
const tabId = msg.tabId || sender.tab?.id;
2374+
const result = await tabChatHandoff.clear(tabId, {
23742375
ownerId: msg.handoffOwnerId,
23752376
handoffGeneration: msg.handoffGeneration,
23762377
});
2378+
if (result?.ok && !result.skipped) {
2379+
browser.runtime.sendMessage({
2380+
target: 'sidepanel',
2381+
action: 'tab_chat_cleared',
2382+
tabId,
2383+
handoffOwnerId: result.handoffOwnerId,
2384+
handoffGeneration: result.handoffGeneration,
2385+
}).catch(() => {});
2386+
}
2387+
return result;
2388+
}
23772389

23782390
case 'list_scheduled_jobs': {
23792391
const tabId = msg.tabId || sender.tab?.id || null;

src/firefox/src/ui/sidepanel.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,9 +1516,10 @@ function clearCachedTabChat(tabId) {
15161516
tabChats.delete(tabId);
15171517
return enqueueTabChatOperation(tabId, async (numericTabId) => {
15181518
tabChats.delete(numericTabId);
1519-
try {
1520-
let handoffGeneration = tabChatHandoffGenerations.get(numericTabId);
1521-
if (!Number.isFinite(handoffGeneration)) {
1519+
let handoffGeneration = tabChatHandoffGenerations.get(numericTabId);
1520+
let clearResult = null;
1521+
while (true) {
1522+
if (!Number.isFinite(handoffGeneration) || clearResult?.reason === 'stale-handoff') {
15221523
const ownership = await sendToBackground('load_tab_chat', {
15231524
tabId: numericTabId,
15241525
waitForHandoff: true,
@@ -1530,13 +1531,13 @@ function clearCachedTabChat(tabId) {
15301531
tabChatHandoffGenerations.set(numericTabId, handoffGeneration);
15311532
}
15321533
}
1533-
await sendToBackground('clear_tab_chat', {
1534+
clearResult = await sendToBackground('clear_tab_chat', {
15341535
tabId: numericTabId,
15351536
handoffOwnerId: tabChatHandoffOwnerId,
15361537
handoffGeneration,
15371538
});
1538-
} catch (e) { /* ignore */ }
1539-
return { ok: true };
1539+
if (!clearResult?.skipped || clearResult.reason !== 'stale-handoff') return clearResult;
1540+
}
15401541
});
15411542
}
15421543

@@ -2220,7 +2221,10 @@ function drainQueuedComposerMessageForCurrentTab() {
22202221
}
22212222

22222223
async function renderClearedConversationForTab(tabId) {
2223-
await clearCachedTabChat(tabId);
2224+
const clearResult = await clearCachedTabChat(tabId);
2225+
if (!clearResult?.ok || clearResult?.skipped) {
2226+
throw new Error(clearResult?.error || 'Unable to clear tab chat.');
2227+
}
22242228
resetComposerHistoryNavigation(tabId);
22252229
saveInputDraftForTab(tabId, '');
22262230
clearPendingAttachmentsForTab(tabId);
@@ -7246,6 +7250,20 @@ browser.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
72467250
});
72477251
});
72487252

7253+
browser.runtime.onMessage.addListener((msg) => {
7254+
if (msg?.target !== 'sidepanel'
7255+
|| msg.action !== 'tab_chat_cleared'
7256+
|| msg.handoffOwnerId === tabChatHandoffOwnerId
7257+
|| document.visibilityState === 'hidden'
7258+
|| !sameTabId(currentTabId, msg.tabId)) return;
7259+
tabChats.delete(Number(msg.tabId));
7260+
if (lastVisibleTabChatSnapshot
7261+
&& sameTabId(lastVisibleTabChatSnapshot.tabId, msg.tabId)) {
7262+
lastVisibleTabChatSnapshot = null;
7263+
}
7264+
requestVisibleSidePanelStateRefresh();
7265+
});
7266+
72497267
document.addEventListener('visibilitychange', () => {
72507268
if (document.visibilityState === 'hidden') {
72517269
visibleStateRefreshPending = false;

test/run.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18223,6 +18223,7 @@ test('chrome sidepanel serializes tab-chat storage writes with clears and reads'
1822318223
assert.match(clearBody, /tabChats\.delete\(tabId\);/, 'chrome: clearing tab chat should delete the cached HTML before queuing storage removal');
1822418224
assert.match(clearBody, /return enqueueTabChatOperation\(tabId, async \(numericTabId\) => \{/, 'chrome: clearing tab chat should be serialized through the queue');
1822518225
assert.match(clearBody, /lastVisibleTabChatSnapshot[\s\S]*?sameTabId\(lastVisibleTabChatSnapshot\.tabId, tabId\)[\s\S]*?lastVisibleTabChatSnapshot = null;[\s\S]*?return enqueueTabChatOperation/, 'chrome: clearing should invalidate the pre-clear handoff snapshot before yielding');
18226+
assert.match(clearBody, /while \(true\)[\s\S]*?clearResult\?\.reason === 'stale-handoff'[\s\S]*?sendToBackground\('load_tab_chat'[\s\S]*?clearResult = await sendToBackground\('clear_tab_chat'[\s\S]*?clearResult\.reason !== 'stale-handoff'/, 'chrome: a stale clear should reacquire ownership and retry before rendering the cleared conversation');
1822618227
assert.match(clearBody, /return enqueueTabChatOperation\(tabId, async \(numericTabId\) => \{[\s\S]*?tabChats\.delete\(numericTabId\);[\s\S]*?handoffGeneration = tabChatHandoffGenerations\.get\(numericTabId\);[\s\S]*?sendToBackground\('load_tab_chat', \{[\s\S]*?waitForHandoff: true,[\s\S]*?sendToBackground\('clear_tab_chat', \{[\s\S]*?handoffOwnerId: tabChatHandoffOwnerId,[\s\S]*?handoffGeneration,/, 'chrome: queued clears should retain or reacquire ownership before clearing shared state');
1822718228
assert.doesNotMatch(clearBody, /tabChatHandoffGenerations\.delete/, 'chrome: clearing content must not discard the visible document handoff generation');
1822818229
});
@@ -18247,6 +18248,7 @@ test('firefox sidepanel serializes tab-chat storage writes with clears and reads
1824718248
assert.match(clearBody, /tabChats\.delete\(tabId\);/, 'firefox: clearing tab chat should delete the cached HTML before queuing storage removal');
1824818249
assert.match(clearBody, /return enqueueTabChatOperation\(tabId, async \(numericTabId\) => \{/, 'firefox: clearing tab chat should be serialized through the queue');
1824918250
assert.match(clearBody, /lastVisibleTabChatSnapshot[\s\S]*?sameTabId\(lastVisibleTabChatSnapshot\.tabId, tabId\)[\s\S]*?lastVisibleTabChatSnapshot = null;[\s\S]*?return enqueueTabChatOperation/, 'firefox: clearing should invalidate the pre-clear handoff snapshot before yielding');
18251+
assert.match(clearBody, /while \(true\)[\s\S]*?clearResult\?\.reason === 'stale-handoff'[\s\S]*?sendToBackground\('load_tab_chat'[\s\S]*?clearResult = await sendToBackground\('clear_tab_chat'[\s\S]*?clearResult\.reason !== 'stale-handoff'/, 'firefox: a stale clear should reacquire ownership and retry before rendering the cleared conversation');
1825018252
assert.match(clearBody, /return enqueueTabChatOperation\(tabId, async \(numericTabId\) => \{[\s\S]*?tabChats\.delete\(numericTabId\);[\s\S]*?handoffGeneration = tabChatHandoffGenerations\.get\(numericTabId\);[\s\S]*?sendToBackground\('load_tab_chat', \{[\s\S]*?waitForHandoff: true,[\s\S]*?sendToBackground\('clear_tab_chat', \{[\s\S]*?handoffOwnerId: tabChatHandoffOwnerId,[\s\S]*?handoffGeneration,/, 'firefox: queued clears should retain or reacquire ownership before clearing shared state');
1825118253
assert.doesNotMatch(clearBody, /tabChatHandoffGenerations\.delete/, 'firefox: clearing content must not discard the visible document handoff generation');
1825218254
});
@@ -21949,6 +21951,16 @@ test('context-menu ownership and stale-panel persistence guards are wired in bot
2194921951
/case 'clear_tab_chat':[\s\S]*?tabChatHandoff\.clear\([\s\S]*?ownerId: msg\.handoffOwnerId,[\s\S]*?handoffGeneration: msg\.handoffGeneration/,
2195021952
`${label}: New Chat should clear content through the visible document's validated handoff ownership`,
2195121953
);
21954+
assert.match(
21955+
background,
21956+
/case 'clear_tab_chat':[\s\S]*?result\?\.ok && !result\.skipped[\s\S]*?action: 'tab_chat_cleared'[\s\S]*?handoffOwnerId: result\.handoffOwnerId/,
21957+
`${label}: a successful clear should notify other panel documents to refresh the cleared transcript`,
21958+
);
21959+
assert.match(
21960+
panel,
21961+
/action !== 'tab_chat_cleared'[\s\S]*?msg\.handoffOwnerId === tabChatHandoffOwnerId[\s\S]*?lastVisibleTabChatSnapshot = null;[\s\S]*?requestVisibleSidePanelStateRefresh\(\);/,
21962+
`${label}: a different visible panel owner should reconcile immediately after a successful clear`,
21963+
);
2195221964
assert.match(
2195321965
background,
2195421966
/requestHandoff:[\s\S]*?tab_chat_handoff_request[\s\S]*?ownerId,[\s\S]*?generation,/,

0 commit comments

Comments
 (0)