Merge branch 'openchamber:main' into github-usage-rework
This commit is contained in:
@@ -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';
|
||||
@@ -23,6 +24,19 @@ export class AgentManagerPanelProvider {
|
||||
private _sseStreams = new Map<string, AbortController>();
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
scheduleCachedStateRetries({
|
||||
target: targetPanel ?? this._panel,
|
||||
getCurrent: () => this._panel,
|
||||
isConnected: () => this._cachedStatus === 'connected',
|
||||
send: () => this._sendCachedState(),
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
@@ -64,6 +78,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 +143,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 {
|
||||
|
||||
@@ -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';
|
||||
@@ -58,6 +59,19 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
private readonly _MESSAGE_TIMEOUT = 5000; // 5 seconds
|
||||
private readonly _MAX_RETRIES = 3;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
scheduleCachedStateRetries({
|
||||
target: targetView ?? this._view,
|
||||
getCurrent: () => this._view,
|
||||
isConnected: () => this._cachedStatus === 'connected',
|
||||
send: () => this._sendCachedState(),
|
||||
});
|
||||
}
|
||||
|
||||
private _createMessageId(): string {
|
||||
return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
@@ -102,6 +116,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 +202,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) {
|
||||
|
||||
@@ -44,6 +44,8 @@ Keep `bridge.ts` as a thin orchestration layer that delegates message handling t
|
||||
|
||||
The webview CSP permits `blob:` only for `worker-src` so shared UI parsers can run bounded local decompression off the main thread. Blob scripts remain disallowed by `script-src`.
|
||||
|
||||
The webview build emits each worker as one self-contained file. VS Code webviews cannot load workers directly from extension resource URLs or load module imports from inside a worker. The shared Shiki client therefore fetches the built worker, starts it from a `blob:` URL, and relies on the worker CSP allowance above.
|
||||
|
||||
- `bridge-localfs-proxy-runtime.ts`
|
||||
- Local `/api/fs/read` and `/api/fs/raw` proxy helpers and shared proxy utility helpers.
|
||||
- Workspace-contained Markdown gallery images use these local filesystem
|
||||
|
||||
@@ -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';
|
||||
@@ -49,6 +50,19 @@ export class SessionEditorPanelProvider {
|
||||
private _lastActiveEditorFilePayload: ActiveEditorFilePayload | null = null;
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
scheduleCachedStateRetries({
|
||||
target: entry.panel,
|
||||
getCurrent: () => this._panels.get(panelId)?.panel,
|
||||
isConnected: () => this._cachedStatus === 'connected',
|
||||
send: () => this._sendCachedStateToPanel(entry),
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
@@ -116,6 +130,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 +204,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 {
|
||||
|
||||
@@ -173,17 +173,20 @@ const readSharedSettingsFromDisk = (): Record<string, unknown> => {
|
||||
};
|
||||
|
||||
const writeSharedSettingsToDisk = async (changes: Record<string, unknown>): Promise<void> => {
|
||||
let tmp: string | null = null;
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(OPENCHAMBER_SHARED_SETTINGS_PATH), { recursive: true });
|
||||
const current = readSharedSettingsFromDisk();
|
||||
const next: Record<string, unknown> = { ...current, ...changes };
|
||||
// Atomic write: tmp file + rename. Readers never see a partial/truncated
|
||||
// JSON that would fail to parse and silently get coerced to {}.
|
||||
const tmp = `${OPENCHAMBER_SHARED_SETTINGS_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
tmp = `${OPENCHAMBER_SHARED_SETTINGS_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
await fs.promises.writeFile(tmp, JSON.stringify(next, null, 2), 'utf8');
|
||||
await fs.promises.rename(tmp, OPENCHAMBER_SHARED_SETTINGS_PATH);
|
||||
} catch {
|
||||
// ignore
|
||||
if (tmp) {
|
||||
await fs.promises.rm(tmp, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
const source = readFileSync(new URL('../vite.config.ts', import.meta.url), 'utf8');
|
||||
|
||||
describe('VS Code webview worker build', () => {
|
||||
test('bundles worker imports into one file', () => {
|
||||
assert.match(source, /worker:\s*\{[\s\S]*?inlineDynamicImports:\s*true/);
|
||||
});
|
||||
});
|
||||
@@ -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<Target>(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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user