Skip to content

Commit 4f5b04b

Browse files
Merge pull request #4 from Smartsheet-JB-Brown/hobbsessr/fix-marketplace-redraw-issue
Hobbsessr/fix marketplace redraw issue
2 parents 2c6ef8a + 62d5589 commit 4f5b04b

File tree

2 files changed

+147
-9
lines changed

2 files changed

+147
-9
lines changed

webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
/**
2+
* MarketplaceViewStateManager
3+
*
4+
* This class manages the state for the marketplace view in the Roo Code extensions interface.
5+
*
6+
* IMPORTANT: Fixed issue where the marketplace feature was causing the Roo Code extensions interface
7+
* to switch to the browse tab and redraw it every 30 seconds. The fix prevents unnecessary tab switching
8+
* and redraws by:
9+
* 1. Only updating the UI when necessary
10+
* 2. Preserving the current tab when handling timeouts
11+
* 3. Using minimal state updates to avoid resetting scroll position
12+
*/
13+
114
import { MarketplaceItem, MarketplaceSource, MatchInfo } from "../../../../src/services/marketplace/types"
215
import { vscode } from "../../utils/vscode"
316
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
@@ -141,11 +154,34 @@ export class MarketplaceViewStateManager {
141154
}
142155
}
143156

144-
private notifyStateChange(): void {
157+
/**
158+
* Notify all registered handlers of a state change
159+
* @param preserveTab If true, ensures the active tab is not changed during notification
160+
*/
161+
private notifyStateChange(preserveTab: boolean = false): void {
145162
const newState = this.getState() // Use getState to ensure proper copying
146-
this.stateChangeHandlers.forEach((handler) => {
147-
handler(newState)
148-
})
163+
164+
if (preserveTab) {
165+
// When preserveTab is true, we're careful not to cause tab switching
166+
// This is used during timeout handling to prevent disrupting the user
167+
this.stateChangeHandlers.forEach((handler) => {
168+
// Store the current active tab
169+
const currentTab = newState.activeTab;
170+
171+
// Create a state update that won't change the active tab
172+
const safeState = {
173+
...newState,
174+
// Don't change these properties to avoid UI disruption
175+
activeTab: currentTab
176+
}
177+
handler(safeState)
178+
})
179+
} else {
180+
// Normal state change notification
181+
this.stateChangeHandlers.forEach((handler) => {
182+
handler(newState)
183+
})
184+
}
149185

150186
// Save state to sessionStorage if available
151187
if (typeof sessionStorage !== "undefined") {
@@ -186,25 +222,47 @@ export class MarketplaceViewStateManager {
186222
}
187223
this.notifyStateChange()
188224

189-
// Set timeout to reset state if fetch takes too long
225+
// Set timeout to reset state if fetch takes too long, but don't trigger a redraw if not needed
190226
this.fetchTimeoutId = setTimeout(() => {
191227
this.clearFetchTimeout()
192228
// On timeout, preserve items if we have them
193229
if (currentItems.length > 0) {
230+
// Only update the isFetching flag without triggering a full redraw
194231
this.state = {
195232
...this.state,
196233
isFetching: false,
197234
allItems: currentItems,
198235
displayItems: currentItems,
199236
}
200237
} else {
238+
// Preserve the current tab and only update necessary state
239+
const { activeTab, sources } = this.state
201240
this.state = {
202241
...this.getDefaultState(),
203-
sources: [...this.state.sources],
204-
activeTab: this.state.activeTab,
242+
sources: [...sources],
243+
activeTab, // Keep the current active tab
205244
}
206245
}
207-
this.notifyStateChange()
246+
247+
// Only notify if we're in the browse tab to avoid switching tabs
248+
if (this.state.activeTab === "browse") {
249+
// Use a minimal state update to avoid resetting scroll position
250+
const handler = (state: ViewState) => {
251+
// Only update the isFetching status without affecting other UI elements
252+
return {
253+
...state,
254+
isFetching: false
255+
}
256+
}
257+
258+
// Call handlers with the minimal update
259+
this.stateChangeHandlers.forEach((stateHandler) => {
260+
stateHandler(handler(this.getState()))
261+
})
262+
} else {
263+
// If not in browse tab, just update the internal state without notifying
264+
// This prevents tab switching
265+
}
208266
}, this.FETCH_TIMEOUT)
209267

210268
break
@@ -560,7 +618,15 @@ export class MarketplaceViewStateManager {
560618
allItems: sortedItems,
561619
displayItems: newDisplayItems,
562620
}
563-
this.notifyStateChange()
621+
622+
// Only notify with full state update if we're in the browse tab
623+
// or if this is the first time we're getting items
624+
if (isOnBrowseTab || !hasCurrentItems) {
625+
this.notifyStateChange()
626+
} else {
627+
// If we're not in the browse tab, update state but don't force a tab switch
628+
this.notifyStateChange(true) // preserve tab
629+
}
564630
}
565631
}
566632

webview-ui/src/components/marketplace/__tests__/MarketplaceViewStateManager.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,78 @@ describe("MarketplaceViewStateManager", () => {
725725
expect(state.allItems).toHaveLength(1)
726726
})
727727

728+
it("should not switch tabs when timeout occurs while in sources tab", async () => {
729+
// First switch to sources tab
730+
await manager.transition({
731+
type: "SET_ACTIVE_TAB",
732+
payload: { tab: "sources" },
733+
})
734+
735+
// Start a fetch
736+
await manager.transition({ type: "FETCH_ITEMS" })
737+
738+
// Set up a state change handler to track tab changes
739+
let tabSwitched = false
740+
const unsubscribe = manager.onStateChange((state) => {
741+
if (state.activeTab === "browse") {
742+
tabSwitched = true
743+
}
744+
})
745+
746+
// Fast-forward past the timeout
747+
jest.advanceTimersByTime(30000)
748+
749+
// Clean up the handler
750+
unsubscribe()
751+
752+
// Verify the tab didn't switch to browse
753+
expect(tabSwitched).toBe(false)
754+
const state = manager.getState()
755+
expect(state.activeTab).toBe("sources")
756+
})
757+
758+
it("should make minimal state updates when timeout occurs in browse tab", async () => {
759+
// First ensure we're in browse tab
760+
await manager.transition({
761+
type: "SET_ACTIVE_TAB",
762+
payload: { tab: "browse" },
763+
})
764+
765+
// Add some items
766+
const testItems = [createTestItem(), createTestItem({ name: "Item 2" })]
767+
await manager.transition({
768+
type: "FETCH_COMPLETE",
769+
payload: { items: testItems },
770+
})
771+
772+
// Start a new fetch
773+
await manager.transition({ type: "FETCH_ITEMS" })
774+
775+
// Track state changes
776+
let stateChangeCount = 0
777+
const unsubscribe = manager.onStateChange(() => {
778+
stateChangeCount++
779+
})
780+
781+
// Reset the counter since we've already had state changes
782+
stateChangeCount = 0
783+
784+
// Fast-forward past the timeout
785+
jest.advanceTimersByTime(30000)
786+
787+
// Clean up the handler
788+
unsubscribe()
789+
790+
// Verify we got a state update
791+
expect(stateChangeCount).toBe(1)
792+
793+
// Verify the items were preserved
794+
const state = manager.getState()
795+
expect(state.allItems).toHaveLength(2)
796+
expect(state.isFetching).toBe(false)
797+
expect(state.activeTab).toBe("browse")
798+
})
799+
728800
it("should prevent concurrent fetches during timeout period", async () => {
729801
jest.clearAllMocks() // Clear mock to ignore initialize() call
730802

0 commit comments

Comments
 (0)