From 0e39cfb79a8b60f6c15b72738ca4ec71873cab65 Mon Sep 17 00:00:00 2001 From: vinciyan Date: Wed, 19 Aug 2026 01:46:46 +0000 Subject: [PATCH 1/2] fix: resend connectionStatus at staggered delays to prevent stuck loading screen on slow networks The webview only leaves its initial loading screen once it receives a connectionStatus=connected postMessage. VS Code drops postMessage calls made before the webview's acquireVsCodeApi bridge is ready, so on slow/remote networks the single send in _sendCachedState() can be lost forever, leaving the sidebar stuck on #initial-loading. Re-send connectionStatus at staggered delays (0.5s-20s) when connected, in ChatViewProvider, AgentManagerPanelProvider and SessionEditorPanelProvider. Stops automatically when the view/panel is disposed or replaced. Message is idempotent. Fixes #2996 --- packages/vscode/package.json | 4 +- .../vscode/src/AgentManagerPanelProvider.ts | 37 +++++++++++++++++++ packages/vscode/src/ChatViewProvider.ts | 37 +++++++++++++++++++ .../vscode/src/SessionEditorPanelProvider.ts | 35 ++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 1eb579fa..e7e7ce77 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -2,7 +2,7 @@ "name": "openchamber", "displayName": "OpenChamber", "description": "%extension.description%", - "version": "1.19.0", + "version": "1.19.1", "publisher": "fedaykindev", "private": true, "repository": { @@ -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 566f9679..e1b068c6 100644 --- a/packages/vscode/src/AgentManagerPanelProvider.ts +++ b/packages/vscode/src/AgentManagerPanelProvider.ts @@ -23,6 +23,33 @@ export class AgentManagerPanelProvider { private _sseStreams = new Map(); 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. + */ + 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); + } + } + constructor( private readonly _context: vscode.ExtensionContext, private readonly _extensionUri: vscode.Uri, @@ -64,6 +91,9 @@ export class AgentManagerPanelProvider { // Send cached connection status this._sendCachedState(); + // The webview bridge may not be ready yet; keep re-sending so a dropped + // `connectionStatus` can never leave the webview stuck on its loading screen. + this._scheduleCachedStateRetries(this._panel); // Handle panel disposal this._panel.onDidDispose(() => { @@ -126,6 +156,13 @@ export class AgentManagerPanelProvider { // Send to webview if it exists this._sendCachedState(); + + // When we become connected, keep re-sending at staggered delays so the + // webview cannot miss the transition (postMessage is dropped if the + // webview bridge is not ready yet). + if (status === 'connected') { + this._scheduleCachedStateRetries(this._panel); + } } public notifySettingsSynced(settings: unknown): void { diff --git a/packages/vscode/src/ChatViewProvider.ts b/packages/vscode/src/ChatViewProvider.ts index e6e7852c..c55d2563 100644 --- a/packages/vscode/src/ChatViewProvider.ts +++ b/packages/vscode/src/ChatViewProvider.ts @@ -58,6 +58,33 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { private readonly _MESSAGE_TIMEOUT = 5000; // 5 seconds 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. + */ + 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); + } + } + private _createMessageId(): string { return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } @@ -102,6 +129,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // Send cached connection status and API URL (may have been set before webview was resolved) this._sendCachedState(); + // The webview bridge may not be ready yet; keep re-sending so a dropped + // `connectionStatus` can never leave the webview stuck on its loading screen. + this._scheduleCachedStateRetries(webviewView); // Send current active editor file state to the new webview this._lastActiveEditorFilePayload = null; @@ -185,6 +215,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // Send to webview if it exists this._sendCachedState(); + + // When we become connected, keep re-sending at staggered delays so the + // webview cannot miss the transition (postMessage is dropped if the + // webview bridge is not ready yet). + if (status === 'connected') { + this._scheduleCachedStateRetries(this._view); + } } public addTextToInput(text: string) { diff --git a/packages/vscode/src/SessionEditorPanelProvider.ts b/packages/vscode/src/SessionEditorPanelProvider.ts index 76a82b02..8c55e401 100644 --- a/packages/vscode/src/SessionEditorPanelProvider.ts +++ b/packages/vscode/src/SessionEditorPanelProvider.ts @@ -49,6 +49,29 @@ export class SessionEditorPanelProvider { private _lastActiveEditorFilePayload: ActiveEditorFilePayload | null = null; 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. + */ + 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); + } + } + constructor( private readonly _context: vscode.ExtensionContext, private readonly _extensionUri: vscode.Uri, @@ -116,6 +139,9 @@ export class SessionEditorPanelProvider { void this.updateTheme(vscode.window.activeColorTheme.kind); this._sendCachedStateToPanel(state); + // The webview bridge may not be ready yet; keep re-sending so a dropped + // `connectionStatus` can never leave the webview stuck on its loading screen. + this._scheduleCachedStateRetries(panelId, state); void this._broadcastActiveEditorFile(); panel.onDidDispose(() => { @@ -187,6 +213,15 @@ export class SessionEditorPanelProvider { for (const entry of this._panels.values()) { this._sendCachedStateToPanel(entry); } + + // When we become connected, keep re-sending at staggered delays so the + // webview cannot miss the transition (postMessage is dropped if the + // webview bridge is not ready yet). + if (status === 'connected') { + for (const [panelId, entry] of this._panels.entries()) { + this._scheduleCachedStateRetries(panelId, entry); + } + } } public notifySettingsSynced(settings: unknown): void { From cc0abad9af2349f7502c14c69caccace8eaa5c6a Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 26 Aug 2026 02:42:47 +0300 Subject: [PATCH 2/2] 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); + } +}