Skip to content

Commit ee490b3

Browse files
authored
Merge pull request #381 from esokullu/main
fix: scope activity status to selected tab
2 parents b0e6bed + 374de6b commit ee490b3

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

src/chrome/src/ui/sidepanel.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,6 +2620,11 @@ async function switchToTab(newTabId) {
26202620
pendingTabSwitch = null;
26212621
tabSwitchTransitionId = newTabId;
26222622
queuedTabSwitchMessages = [];
2623+
// The activity strip is a single panel-wide DOM node, unlike the tab-scoped
2624+
// chat and run journals. Clear the outgoing tab's transient status before
2625+
// any async restore work can yield; restoreActiveRunState (or a queued target
2626+
// update) will show it again if the destination tab is actually running.
2627+
hideActivity();
26232628

26242629
try {
26252630
// Save the tab currently represented by the DOM. During an async restore,
@@ -2754,6 +2759,11 @@ async function restoreActiveRunState(tabId = currentTabId) {
27542759
data: { status: runUi.status, finalContent: runUi.finalContent },
27552760
});
27562761
}
2762+
// Terminal snapshots may already be fully acknowledged, so no replayed
2763+
// run_complete event remains to clear the shared activity strip. Make the
2764+
// destination tab's idle state authoritative even in that case.
2765+
hideActivity();
2766+
syncSendButtonState();
27572767
}
27582768
}
27592769

src/firefox/src/ui/sidepanel.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2579,6 +2579,11 @@ async function switchToTab(newTabId) {
25792579
pendingTabSwitch = null;
25802580
tabSwitchTransitionId = newTabId;
25812581
queuedTabSwitchMessages = [];
2582+
// The activity strip is a single panel-wide DOM node, unlike the tab-scoped
2583+
// chat and run journals. Clear the outgoing tab's transient status before
2584+
// any async restore work can yield; restoreActiveRunState (or a queued target
2585+
// update) will show it again if the destination tab is actually running.
2586+
hideActivity();
25822587

25832588
try {
25842589
// Save the tab currently represented by the DOM. During an async restore,
@@ -2700,6 +2705,11 @@ async function restoreActiveRunState(tabId = currentTabId) {
27002705
data: { status: runUi.status, finalContent: runUi.finalContent },
27012706
});
27022707
}
2708+
// Terminal snapshots may already be fully acknowledged, so no replayed
2709+
// run_complete event remains to clear the shared activity strip. Make the
2710+
// destination tab's idle state authoritative even in that case.
2711+
hideActivity();
2712+
syncSendButtonState();
27032713
}
27042714
}
27052715

test/run.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9648,6 +9648,55 @@ test('sidepanel queues target-tab updates and suppresses non-target updates duri
96489648
}
96499649
});
96509650

9651+
test('sidepanel clears shared activity while restoring an idle destination tab', () => {
9652+
for (const [label, panelRel] of [
9653+
['chrome', 'src/chrome/src/ui/sidepanel.js'],
9654+
['firefox', 'src/firefox/src/ui/sidepanel.js'],
9655+
]) {
9656+
const panel = fs.readFileSync(path.join(ROOT, panelRel), 'utf8');
9657+
const switchMatch = panel.match(/async function switchToTab\(newTabId\) \{([\s\S]*?)\n\}/);
9658+
assert.ok(switchMatch, `${label}: switchToTab body missing`);
9659+
const switchBody = switchMatch[1];
9660+
const transitionIdx = switchBody.indexOf('tabSwitchTransitionId = newTabId;');
9661+
const hideIdx = switchBody.indexOf('hideActivity();', transitionIdx);
9662+
const firstAwaitIdx = switchBody.indexOf('await flushRenderedTabChat();');
9663+
const replayIdx = switchBody.indexOf('drainQueuedAgentUpdatesForTab(newTabId);');
9664+
assert.notEqual(hideIdx, -1, `${label}: tab switches should clear the outgoing activity strip`);
9665+
assert.notEqual(firstAwaitIdx, -1, `${label}: tab-switch async restore boundary missing`);
9666+
assert.notEqual(replayIdx, -1, `${label}: queued destination updates should still replay`);
9667+
assert.equal(transitionIdx < hideIdx && hideIdx < firstAwaitIdx, true, `${label}: outgoing activity must clear before async tab restore can yield`);
9668+
assert.equal(hideIdx < replayIdx, true, `${label}: queued destination updates must replay after the transient reset`);
9669+
9670+
const restoreStart = panel.indexOf('async function restoreActiveRunState(tabId = currentTabId)');
9671+
const restoreEnd = panel.indexOf('\nfunction conversationHasUserMessages()', restoreStart);
9672+
assert.notEqual(restoreStart, -1, `${label}: active-run restore helper missing`);
9673+
assert.notEqual(restoreEnd, -1, `${label}: active-run restore helper boundary missing`);
9674+
const restoreBody = panel.slice(restoreStart, restoreEnd);
9675+
const runningIdx = restoreBody.indexOf('if (state?.running) {');
9676+
const idleIdx = restoreBody.indexOf('} else {', runningIdx);
9677+
const runningBody = restoreBody.slice(runningIdx, idleIdx);
9678+
const idleBody = restoreBody.slice(idleIdx);
9679+
const clearProcessingIdx = idleBody.indexOf('setTabProcessing(numericTabId, false);');
9680+
const idleHideIdx = idleBody.indexOf('hideActivity();');
9681+
const syncComposerIdx = idleBody.indexOf('syncSendButtonState();');
9682+
assert.match(runningBody, /showActivity\(t\('sp\.activity\.thinking'\)\);/, `${label}: a running destination should restore its activity strip`);
9683+
assert.notEqual(clearProcessingIdx, -1, `${label}: idle restore should clear destination processing state`);
9684+
assert.notEqual(idleHideIdx, -1, `${label}: idle restore should hide stale activity even when terminal replay is already acknowledged`);
9685+
assert.notEqual(syncComposerIdx, -1, `${label}: idle restore should resync composer controls`);
9686+
assert.equal(clearProcessingIdx < idleHideIdx && idleHideIdx < syncComposerIdx, true, `${label}: idle activity and composer state should settle after processing is cleared`);
9687+
9688+
const drainHelperStart = panel.indexOf('function drainQueuedAgentUpdatesForTab(tabId)');
9689+
const drainHelperEnd = panel.indexOf('\nasync function settleScheduledRun', drainHelperStart);
9690+
const drainHelperBody = panel.slice(drainHelperStart, drainHelperEnd);
9691+
assert.match(drainHelperBody, /replay\.forEach\(\(msg\) => handleAgentUpdateMessage\(msg\)\);/, `${label}: queued destination updates should re-enter the normal renderer`);
9692+
const updateHandlerStart = panel.indexOf('function handleAgentUpdateMessage(msg)');
9693+
const thinkingStart = panel.indexOf("case 'thinking':", updateHandlerStart);
9694+
const textStart = panel.indexOf("case 'text':", thinkingStart);
9695+
const thinkingBody = panel.slice(thinkingStart, textStart);
9696+
assert.match(thinkingBody, /showActivity\(/, `${label}: a queued thinking update should be able to show activity again after reset`);
9697+
}
9698+
});
9699+
96519700
test('sidepanel hydrates restored history ids before fallback records', () => {
96529701
for (const [label, panelRel] of [
96539702
['chrome', 'src/chrome/src/ui/sidepanel.js'],

0 commit comments

Comments
 (0)