|
| 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 | + |
1 | 14 | import { MarketplaceItem, MarketplaceSource, MatchInfo } from "../../../../src/services/marketplace/types" |
2 | 15 | import { vscode } from "../../utils/vscode" |
3 | 16 | import { WebviewMessage } from "../../../../src/shared/WebviewMessage" |
@@ -141,11 +154,34 @@ export class MarketplaceViewStateManager { |
141 | 154 | } |
142 | 155 | } |
143 | 156 |
|
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 { |
145 | 162 | 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 | + } |
149 | 185 |
|
150 | 186 | // Save state to sessionStorage if available |
151 | 187 | if (typeof sessionStorage !== "undefined") { |
@@ -186,25 +222,47 @@ export class MarketplaceViewStateManager { |
186 | 222 | } |
187 | 223 | this.notifyStateChange() |
188 | 224 |
|
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 |
190 | 226 | this.fetchTimeoutId = setTimeout(() => { |
191 | 227 | this.clearFetchTimeout() |
192 | 228 | // On timeout, preserve items if we have them |
193 | 229 | if (currentItems.length > 0) { |
| 230 | + // Only update the isFetching flag without triggering a full redraw |
194 | 231 | this.state = { |
195 | 232 | ...this.state, |
196 | 233 | isFetching: false, |
197 | 234 | allItems: currentItems, |
198 | 235 | displayItems: currentItems, |
199 | 236 | } |
200 | 237 | } else { |
| 238 | + // Preserve the current tab and only update necessary state |
| 239 | + const { activeTab, sources } = this.state |
201 | 240 | this.state = { |
202 | 241 | ...this.getDefaultState(), |
203 | | - sources: [...this.state.sources], |
204 | | - activeTab: this.state.activeTab, |
| 242 | + sources: [...sources], |
| 243 | + activeTab, // Keep the current active tab |
205 | 244 | } |
206 | 245 | } |
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 | + } |
208 | 266 | }, this.FETCH_TIMEOUT) |
209 | 267 |
|
210 | 268 | break |
@@ -560,7 +618,15 @@ export class MarketplaceViewStateManager { |
560 | 618 | allItems: sortedItems, |
561 | 619 | displayItems: newDisplayItems, |
562 | 620 | } |
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 | + } |
564 | 630 | } |
565 | 631 | } |
566 | 632 |
|
|
0 commit comments