From cc0abad9af2349f7502c14c69caccace8eaa5c6a Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 26 Aug 2026 02:42:47 +0300 Subject: [PATCH] refactor(vscode): share the cached-state retry schedule across providers The staggered re-send lived as three near-identical private methods in the chat view, agent manager, and session editor providers. One helper now owns the delays, the connected gate, and the replaced-target guard; the providers keep thin wrappers over their own panel/view identity. Also restores the package.json trailing newline the branch dropped. --- packages/vscode/package.json | 2 +- .../vscode/src/AgentManagerPanelProvider.ts | 31 ++++++------------- packages/vscode/src/ChatViewProvider.ts | 31 ++++++------------- .../vscode/src/SessionEditorPanelProvider.ts | 27 ++++++---------- .../vscode/src/webviewCachedStateRetry.ts | 31 +++++++++++++++++++ 5 files changed, 59 insertions(+), 63 deletions(-) create mode 100644 packages/vscode/src/webviewCachedStateRetry.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 0f5e0cca..4fa8976c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -252,4 +252,4 @@ "react-dom": "^19.1.1", "yaml": "^2.8.1" } -} \ No newline at end of file +} diff --git a/packages/vscode/src/AgentManagerPanelProvider.ts b/packages/vscode/src/AgentManagerPanelProvider.ts index e1b068c6..e19ed9b7 100644 --- a/packages/vscode/src/AgentManagerPanelProvider.ts +++ b/packages/vscode/src/AgentManagerPanelProvider.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { scheduleCachedStateRetries } from './webviewCachedStateRetry'; import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge'; import { getThemeKindName } from './theme'; import type { OpenCodeManager, ConnectionStatus } from './opencode'; @@ -24,30 +25,16 @@ export class AgentManagerPanelProvider { private readonly _webviewDevServerUrl: string | null; /** - * The webview only leaves its initial loading screen once it receives a - * `connectionStatus: connected` message. VS Code drops postMessage calls - * made before the webview's acquireVsCodeApi bridge is ready (common in - * code-server / slow or flaky networks), so a single send can be lost - * forever. Re-send at staggered delays until the target panel is replaced. + * See webviewCachedStateRetry.ts — a single postMessage can be dropped + * before the webview bridge is ready, leaving the loading screen stuck. */ private _scheduleCachedStateRetries(targetPanel: vscode.WebviewPanel | undefined): void { - if (this._cachedStatus !== 'connected') { - return; - } - const panel = targetPanel ?? this._panel; - if (!panel) { - return; - } - const delaysMs = [500, 1500, 3500, 7000, 12000, 20000]; - for (const delayMs of delaysMs) { - setTimeout(() => { - // Only re-send if this exact panel is still the active one. - if (this._panel !== panel) { - return; - } - this._sendCachedState(); - }, delayMs); - } + scheduleCachedStateRetries({ + target: targetPanel ?? this._panel, + getCurrent: () => this._panel, + isConnected: () => this._cachedStatus === 'connected', + send: () => this._sendCachedState(), + }); } constructor( diff --git a/packages/vscode/src/ChatViewProvider.ts b/packages/vscode/src/ChatViewProvider.ts index c55d2563..ab547fa3 100644 --- a/packages/vscode/src/ChatViewProvider.ts +++ b/packages/vscode/src/ChatViewProvider.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { scheduleCachedStateRetries } from './webviewCachedStateRetry'; import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge'; import { getThemeKindName } from './theme'; import type { OpenCodeManager, ConnectionStatus } from './opencode'; @@ -59,30 +60,16 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { private readonly _MAX_RETRIES = 3; /** - * The webview only leaves its initial loading screen once it receives a - * `connectionStatus: connected` message. VS Code drops postMessage calls - * made before the webview's acquireVsCodeApi bridge is ready (common in - * code-server / slow or flaky networks), so a single send can be lost - * forever. Re-send at staggered delays until the target view is replaced. + * See webviewCachedStateRetry.ts — a single postMessage can be dropped + * before the webview bridge is ready, leaving the loading screen stuck. */ private _scheduleCachedStateRetries(targetView: vscode.WebviewView | undefined): void { - if (this._cachedStatus !== 'connected') { - return; - } - const view = targetView ?? this._view; - if (!view) { - return; - } - const delaysMs = [500, 1500, 3500, 7000, 12000, 20000]; - for (const delayMs of delaysMs) { - setTimeout(() => { - // Only re-send if this exact view is still the active one. - if (this._view !== view) { - return; - } - this._sendCachedState(); - }, delayMs); - } + scheduleCachedStateRetries({ + target: targetView ?? this._view, + getCurrent: () => this._view, + isConnected: () => this._cachedStatus === 'connected', + send: () => this._sendCachedState(), + }); } private _createMessageId(): string { diff --git a/packages/vscode/src/SessionEditorPanelProvider.ts b/packages/vscode/src/SessionEditorPanelProvider.ts index 8c55e401..6150e13d 100644 --- a/packages/vscode/src/SessionEditorPanelProvider.ts +++ b/packages/vscode/src/SessionEditorPanelProvider.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { scheduleCachedStateRetries } from './webviewCachedStateRetry'; import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge'; import { getThemeKindName } from './theme'; import type { OpenCodeManager, ConnectionStatus } from './opencode'; @@ -50,26 +51,16 @@ export class SessionEditorPanelProvider { private readonly _webviewDevServerUrl: string | null; /** - * The webview only leaves its initial loading screen once it receives a - * `connectionStatus: connected` message. VS Code drops postMessage calls - * made before the webview's acquireVsCodeApi bridge is ready (common in - * code-server / slow or flaky networks), so a single send can be lost - * forever. Re-send at staggered delays until the target panel is replaced. + * See webviewCachedStateRetry.ts — a single postMessage can be dropped + * before the webview bridge is ready, leaving the loading screen stuck. */ private _scheduleCachedStateRetries(panelId: string, entry: SessionPanelState): void { - if (this._cachedStatus !== 'connected') { - return; - } - const delaysMs = [500, 1500, 3500, 7000, 12000, 20000]; - for (const delayMs of delaysMs) { - setTimeout(() => { - // Only re-send if this exact panel is still registered. - if (this._panels.get(panelId)?.panel !== entry.panel) { - return; - } - this._sendCachedStateToPanel(entry); - }, delayMs); - } + scheduleCachedStateRetries({ + target: entry.panel, + getCurrent: () => this._panels.get(panelId)?.panel, + isConnected: () => this._cachedStatus === 'connected', + send: () => this._sendCachedStateToPanel(entry), + }); } constructor( diff --git a/packages/vscode/src/webviewCachedStateRetry.ts b/packages/vscode/src/webviewCachedStateRetry.ts new file mode 100644 index 00000000..f750c344 --- /dev/null +++ b/packages/vscode/src/webviewCachedStateRetry.ts @@ -0,0 +1,31 @@ +/** + * The webview only leaves its initial loading screen once it receives a + * `connectionStatus: connected` message. VS Code drops postMessage calls made + * before the webview's acquireVsCodeApi bridge is ready (common in + * code-server / slow or flaky networks), so a single send can be lost + * forever. Re-sending the cached state at staggered delays bounds the wait + * without needing a webview-side ack protocol; the payload is idempotent + * (connection status + window focus), so duplicate deliveries are harmless. + */ +const CACHED_STATE_RETRY_DELAYS_MS = [500, 1500, 3500, 7000, 12000, 20000]; + +export function scheduleCachedStateRetries(input: { + /** The panel/view the retries belong to. */ + target: Target | undefined; + /** Reads the provider's CURRENT panel/view, so a replaced target stops its stale retries. */ + getCurrent: () => Target | undefined; + /** Retries only make sense for the connected transition. */ + isConnected: () => boolean; + /** Re-sends the provider's cached state. */ + send: () => void; +}): void { + if (!input.isConnected()) return; + const target = input.target; + if (!target) return; + for (const delayMs of CACHED_STATE_RETRY_DELAYS_MS) { + setTimeout(() => { + if (input.getCurrent() !== target) return; + input.send(); + }, delayMs); + } +}