feat: improve VS Code dev flow and stabilize sidebar/chat behavior (#754)
* fix: improve session sidebar tooltip and truncation behavior - Keep new-draft tooltip anchored to its trigger button - Fix minimal-mode worktree/group header text truncation - Tune minimal-mode right padding to reduce early label clipping * fix: render reasoning through markdown pipeline - Use Streamdown rendering for reasoning in live chat mode - Remove italic styling from reasoning text - Render expanded reasoning content with MarkdownRenderer * chore: remove legacy electron dependencies - Removed unused Electron packages from root and UI manifests - Deleted obsolete Electron context menu type declaration - Regenerated lockfile after dependency cleanup * fix: handle non-repository folders in git status API - Prevent 500 errors when status is requested outside a valid Git repo - Improve repository detection using `git rev-parse --git-dir` - Reduce noisy server logs for expected non-repo status checks * fix unloaded session chat layout flicker * fix: reduce noisy TTS status polling Cache and dedupe TTS status requests, and only check provider availability when the related voice features are enabled so disabled voice setups stay quiet. * perf: throttle background PR git status refreshes * fix: improve VS Code Explorer file drop mentions in chat - Add Explorer context action to insert selected files as @mentions. - Handle Explorer drag-and-drop to prefill @file mentions instead of attachments. - Prevent duplicate plain-path text when dropping multiple files. * fix: deduplicate recent sessions in VS Code sidebar - Hide sessions from main list when already shown in recent - Apply dedup only in VS Code runtime - Keep session search behavior unchanged * feat: add true HMR dev flow for VS Code extension - Load VS Code webview from Vite dev server with React refresh preamble - Add `vscode:dev` runner that starts watchers and opens Extension Development Host - Update VS Code dev docs and scripts to use the new HMR startup flow * feat: polish VS Code session sidebar and attachment UX - Add resizable sessions sidebar in VS Code layout - Tighten session list spacing and hover behavior in VS Code - Remove bulk file/image attach success toasts while keeping error toasts
This commit is contained in:
committed by
GitHub
parent
ea6d4c4d43
commit
1231fd773e
@@ -62,6 +62,20 @@ Select code in the editor, right-click, and find the **OpenChamber** submenu:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run vscode:dev
|
||||
```
|
||||
|
||||
`bun run vscode:dev` now starts watchers + opens an Extension Development Host automatically. Webview UI changes use Vite HMR automatically.
|
||||
|
||||
Optional overrides:
|
||||
|
||||
- `OPENCHAMBER_VSCODE_BIN=cursor bun run vscode:dev`
|
||||
- `OPENCHAMBER_VSCODE_DEV_WORKSPACE=/path/to/workspace bun run vscode:dev`
|
||||
- `bun run vscode:dev /path/to/workspace`
|
||||
|
||||
To package manually:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/vscode build
|
||||
cd packages/vscode && bunx vsce package --no-dependencies
|
||||
```
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"main": "./dist/extension.js",
|
||||
"activationEvents": [
|
||||
"onCommand:openchamber.openSidebar",
|
||||
"onCommand:openchamber.attachExplorerToChat",
|
||||
"onView:openchamber.chatView"
|
||||
],
|
||||
"contributes": {
|
||||
@@ -134,6 +135,11 @@
|
||||
"category": "OpenChamber",
|
||||
"title": "Settings",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.attachExplorerToChat",
|
||||
"category": "OpenChamber",
|
||||
"title": "Attach to OpenChamber Chat"
|
||||
}
|
||||
],
|
||||
"submenus": [
|
||||
@@ -149,6 +155,13 @@
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "openchamber.attachExplorerToChat",
|
||||
"when": "resourceScheme == file && explorerResourceIsFolder == false",
|
||||
"group": "navigation@50"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "openchamber.openNewSessionInEditor",
|
||||
@@ -210,7 +223,8 @@
|
||||
"build": "bun run build:extension && bun run build:webview",
|
||||
"build:extension": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --minify --main-fields=module,main",
|
||||
"build:webview": "VITE_OPENCODE_URL=/api vite build",
|
||||
"dev": "concurrently -n \"ext,web\" -c \"cyan,magenta\" \"bun run watch:extension\" \"bun run watch:webview\"",
|
||||
"dev": "concurrently -n \"ext,web\" -c \"cyan,magenta\" \"bun run watch:extension\" \"bun run dev:webview\"",
|
||||
"dev:webview": "vite --host localhost --port 5173 --strictPort",
|
||||
"watch:extension": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --watch --sourcemap --main-fields=module,main",
|
||||
"watch:webview": "vite build --watch",
|
||||
"type-check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.webview.json",
|
||||
@@ -229,7 +243,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@openchamber/ui": "workspace:*",
|
||||
"@opencode-ai/sdk": "^1.2.27",
|
||||
"@opencode-ai/sdk": "^1.3.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"react": "^19.1.1",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenCodeManager, ConnectionStatus } from './opencode';
|
||||
import { getWebviewShikiThemes } from './shikiThemes';
|
||||
import { getWebviewHtml } from './webviewHtml';
|
||||
import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
|
||||
export class AgentManagerPanelProvider {
|
||||
public static readonly viewType = 'openchamber.agentManager';
|
||||
@@ -16,12 +17,15 @@ export class AgentManagerPanelProvider {
|
||||
private _cachedError?: string;
|
||||
private _sseCounter = 0;
|
||||
private _sseStreams = new Map<string, AbortController>();
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
private readonly _openCodeManager?: OpenCodeManager
|
||||
) {}
|
||||
) {
|
||||
this._webviewDevServerUrl = resolveWebviewDevServerUrl(this._context);
|
||||
}
|
||||
|
||||
public createOrShow(): void {
|
||||
// If panel exists, reveal it
|
||||
@@ -227,6 +231,7 @@ export class AgentManagerPanelProvider {
|
||||
initialStatus: this._cachedStatus,
|
||||
cliAvailable,
|
||||
panelType: 'agentManager',
|
||||
devServerUrl: this._webviewDevServerUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenCodeManager, ConnectionStatus } from './opencode';
|
||||
import { getWebviewShikiThemes } from './shikiThemes';
|
||||
import { getWebviewHtml } from './webviewHtml';
|
||||
import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
|
||||
export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewType = 'openchamber.chatView';
|
||||
@@ -20,12 +21,15 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
private _cachedError?: string;
|
||||
private _sseCounter = 0;
|
||||
private _sseStreams = new Map<string, AbortController>();
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
private readonly _openCodeManager?: OpenCodeManager
|
||||
) {}
|
||||
) {
|
||||
this._webviewDevServerUrl = resolveWebviewDevServerUrl(this._context);
|
||||
}
|
||||
|
||||
public resolveWebviewView(
|
||||
webviewView: vscode.WebviewView
|
||||
@@ -110,6 +114,27 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public addFileMentions(paths: string[]) {
|
||||
if (!this._view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedPaths = paths
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
|
||||
if (cleanedPaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._view.show(true);
|
||||
this._view.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'addFileMentions',
|
||||
payload: { paths: cleanedPaths },
|
||||
});
|
||||
}
|
||||
|
||||
public createNewSessionWithPrompt(prompt: string) {
|
||||
if (this._view) {
|
||||
// Reveal the webview panel
|
||||
@@ -277,6 +302,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
workspaceFolder,
|
||||
initialStatus,
|
||||
cliAvailable,
|
||||
devServerUrl: this._webviewDevServerUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenCodeManager, ConnectionStatus } from './opencode';
|
||||
import { getWebviewShikiThemes } from './shikiThemes';
|
||||
import { getWebviewHtml } from './webviewHtml';
|
||||
import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
|
||||
type SessionPanelState = {
|
||||
panel: vscode.WebviewPanel;
|
||||
@@ -18,12 +19,15 @@ export class SessionEditorPanelProvider {
|
||||
private _cachedError?: string;
|
||||
private _sseCounter = 0;
|
||||
private _panels = new Map<string, SessionPanelState>();
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
private readonly _openCodeManager?: OpenCodeManager
|
||||
) {}
|
||||
) {
|
||||
this._webviewDevServerUrl = resolveWebviewDevServerUrl(this._context);
|
||||
}
|
||||
|
||||
public createOrShowNewSession(): void {
|
||||
// Generate unique panel ID for new session drafts
|
||||
@@ -266,6 +270,7 @@ export class SessionEditorPanelProvider {
|
||||
panelType: 'chat',
|
||||
initialSessionId: sessionId ?? undefined,
|
||||
viewMode: 'editor',
|
||||
devServerUrl: this._webviewDevServerUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,50 @@ const guessMimeTypeFromExtension = (ext: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasUriScheme = (value: string): boolean => /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
|
||||
|
||||
const parseDroppedFileReference = (rawReference: string):
|
||||
| { uri: vscode.Uri }
|
||||
| { skipped: { name: string; reason: string } } => {
|
||||
const trimmed = rawReference.trim().replace(/^['"]+|['"]+$/g, '');
|
||||
if (!trimmed) {
|
||||
return { skipped: { name: rawReference, reason: 'Empty drop reference' } };
|
||||
}
|
||||
|
||||
if (hasUriScheme(trimmed)) {
|
||||
try {
|
||||
const parsed = vscode.Uri.parse(trimmed, true);
|
||||
if (parsed.scheme !== 'file') {
|
||||
return {
|
||||
skipped: {
|
||||
name: trimmed,
|
||||
reason: `Unsupported URI scheme: ${parsed.scheme || 'unknown'}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { uri: parsed };
|
||||
} catch (error) {
|
||||
return {
|
||||
skipped: {
|
||||
name: trimmed,
|
||||
reason: error instanceof Error ? error.message : 'Invalid URI',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(trimmed)) {
|
||||
return {
|
||||
skipped: {
|
||||
name: trimmed,
|
||||
reason: 'Drop reference is not an absolute file path',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { uri: vscode.Uri.file(trimmed) };
|
||||
};
|
||||
|
||||
const readUriAsAttachment = async (
|
||||
uri: vscode.Uri,
|
||||
fallbackName?: string,
|
||||
@@ -1934,24 +1978,13 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
const dedupedUris = Array.from(new Set(uris.map((value) => value.trim())));
|
||||
|
||||
for (const rawUri of dedupedUris) {
|
||||
let uri: vscode.Uri;
|
||||
try {
|
||||
uri = vscode.Uri.parse(rawUri, true);
|
||||
} catch (error) {
|
||||
skipped.push({
|
||||
name: rawUri,
|
||||
reason: error instanceof Error ? error.message : 'Invalid URI',
|
||||
});
|
||||
const parsed = parseDroppedFileReference(rawUri);
|
||||
if ('skipped' in parsed) {
|
||||
skipped.push(parsed.skipped);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uri.scheme !== 'file') {
|
||||
skipped.push({
|
||||
name: rawUri,
|
||||
reason: `Unsupported URI scheme: ${uri.scheme}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const uri = parsed.uri;
|
||||
|
||||
const name = path.basename(uri.fsPath || uri.path || rawUri);
|
||||
|
||||
|
||||
@@ -269,6 +269,66 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('openchamber.attachExplorerToChat', async (resource?: vscode.Uri, resources?: vscode.Uri[]) => {
|
||||
const uriCandidates: vscode.Uri[] = [];
|
||||
if (Array.isArray(resources)) {
|
||||
uriCandidates.push(...resources.filter((entry): entry is vscode.Uri => entry instanceof vscode.Uri));
|
||||
}
|
||||
if (resource instanceof vscode.Uri) {
|
||||
uriCandidates.push(resource);
|
||||
}
|
||||
if (uriCandidates.length === 0) {
|
||||
const activeEditorUri = vscode.window.activeTextEditor?.document.uri;
|
||||
if (activeEditorUri) {
|
||||
uriCandidates.push(activeEditorUri);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueUris = Array.from(new Map(uriCandidates.map((uri) => [uri.toString(), uri])).values());
|
||||
const mentionPaths: string[] = [];
|
||||
const skippedEntries: string[] = [];
|
||||
|
||||
for (const uri of uniqueUris) {
|
||||
if (uri.scheme !== 'file') {
|
||||
skippedEntries.push(uri.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(uri);
|
||||
if ((stat.type & vscode.FileType.Directory) !== 0) {
|
||||
skippedEntries.push(vscode.workspace.asRelativePath(uri, false));
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
skippedEntries.push(vscode.workspace.asRelativePath(uri, false));
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = vscode.workspace.asRelativePath(uri, false).replace(/\\/g, '/').trim();
|
||||
if (!relativePath) {
|
||||
skippedEntries.push(uri.fsPath || uri.toString());
|
||||
continue;
|
||||
}
|
||||
mentionPaths.push(relativePath);
|
||||
}
|
||||
|
||||
if (mentionPaths.length === 0) {
|
||||
vscode.window.showWarningMessage('OpenChamber: No file selected to mention');
|
||||
return;
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('openchamber.openSidebar');
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
chatViewProvider?.addFileMentions(mentionPaths);
|
||||
|
||||
if (skippedEntries.length > 0) {
|
||||
vscode.window.showInformationMessage('OpenChamber: Some selected entries were skipped (folders or unsupported resources)');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('openchamber.explain', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const DEFAULT_WEBVIEW_DEV_SERVER_URL = 'http://localhost:5173';
|
||||
|
||||
const normalizeUrl = (value: string): string | null => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return null;
|
||||
}
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveWebviewDevServerUrl = (context: vscode.ExtensionContext): string | null => {
|
||||
if (context.extensionMode !== vscode.ExtensionMode.Development) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (process.env.OPENCHAMBER_DISABLE_WEBVIEW_HMR === '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configured = normalizeUrl(process.env.OPENCHAMBER_VSCODE_WEBVIEW_URL ?? '');
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
|
||||
return DEFAULT_WEBVIEW_DEV_SERVER_URL;
|
||||
};
|
||||
@@ -13,8 +13,32 @@ export interface WebviewHtmlOptions {
|
||||
panelType?: PanelType;
|
||||
initialSessionId?: string;
|
||||
viewMode?: 'sidebar' | 'editor';
|
||||
devServerUrl?: string | null;
|
||||
}
|
||||
|
||||
const asCspToken = (value: string | null | undefined): string | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
};
|
||||
|
||||
const toOrigin = (value: string | null | undefined): string | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(value).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const uniqueTokens = (values: Array<string | null | undefined>): string => {
|
||||
return Array.from(new Set(values.map(asCspToken).filter((value): value is string => Boolean(value)))).join(' ');
|
||||
};
|
||||
|
||||
export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
const {
|
||||
webview,
|
||||
@@ -25,10 +49,18 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
panelType = 'chat',
|
||||
initialSessionId,
|
||||
viewMode = 'sidebar',
|
||||
devServerUrl,
|
||||
} = options;
|
||||
|
||||
const scriptPath = vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'assets', 'index.js');
|
||||
const scriptUri = webview.asWebviewUri(scriptPath);
|
||||
const normalizedDevServerUrl = asCspToken(devServerUrl)?.replace(/\/$/, '') ?? null;
|
||||
const devServerOrigin = toOrigin(normalizedDevServerUrl);
|
||||
const styleSrc = uniqueTokens([webview.cspSource, "'unsafe-inline'", devServerOrigin]);
|
||||
const scriptSrc = uniqueTokens([webview.cspSource, "'unsafe-inline'", "'unsafe-eval'", devServerOrigin]);
|
||||
const connectSrc = uniqueTokens(['*', 'ws:', 'wss:', 'http:', 'https:', devServerOrigin]);
|
||||
const imgSrc = uniqueTokens([webview.cspSource, 'data:', 'https:', devServerOrigin]);
|
||||
const fontSrc = uniqueTokens([webview.cspSource, 'data:', devServerOrigin]);
|
||||
|
||||
const themeKind = getThemeKindName(vscode.window.activeColorTheme.kind);
|
||||
|
||||
@@ -45,7 +77,7 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource} 'unsafe-inline' 'unsafe-eval'; connect-src * ws: wss: http: https:; img-src ${webview.cspSource} data: https:; font-src ${webview.cspSource} data:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${styleSrc}; script-src ${scriptSrc}; connect-src ${connectSrc}; img-src ${imgSrc}; font-src ${fontSrc};">
|
||||
<style>
|
||||
html, body, #root { height: 100%; width: 100%; margin: 0; padding: 0; }
|
||||
body {
|
||||
@@ -171,7 +203,105 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="module" src="${scriptUri}"></script>
|
||||
<script type="module">
|
||||
const prodEntryUrl = ${JSON.stringify(scriptUri.toString())};
|
||||
const devServerUrl = ${normalizedDevServerUrl ? JSON.stringify(normalizedDevServerUrl) : 'null'};
|
||||
|
||||
const loadProductionBundle = () => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = prodEntryUrl;
|
||||
document.body.appendChild(script);
|
||||
};
|
||||
|
||||
if (!devServerUrl) {
|
||||
loadProductionBundle();
|
||||
} else {
|
||||
const baseUrl = devServerUrl;
|
||||
|
||||
const statusEl = document.getElementById('loading-status');
|
||||
const setStatus = (text) => {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
};
|
||||
|
||||
const retryDelayMs = 500;
|
||||
let attempt = 0;
|
||||
|
||||
const waitForRootMount = (timeoutMs) => {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (root.childNodes.length > 0) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (root.childNodes.length > 0) {
|
||||
observer.disconnect();
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(root, { childList: true, subtree: true });
|
||||
const timer = window.setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(root.childNodes.length > 0);
|
||||
}, timeoutMs);
|
||||
});
|
||||
};
|
||||
|
||||
const tryLoadDevBundle = () => {
|
||||
const viteClientUrl = baseUrl + '/@vite/client';
|
||||
const reactRefreshUrl = baseUrl + '/@react-refresh';
|
||||
const devEntryUrl = baseUrl + '/main.tsx';
|
||||
const hostLabel = (() => {
|
||||
try {
|
||||
return new URL(baseUrl).host;
|
||||
} catch {
|
||||
return baseUrl;
|
||||
}
|
||||
})();
|
||||
|
||||
setStatus('Starting webview dev server (' + hostLabel + ')...');
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => import(viteClientUrl))
|
||||
.then(() => import(reactRefreshUrl))
|
||||
.then((mod) => {
|
||||
const runtime = mod && mod.default ? mod.default : null;
|
||||
if (runtime && typeof runtime.injectIntoGlobalHook === 'function') {
|
||||
runtime.injectIntoGlobalHook(window);
|
||||
window.$RefreshReg$ = () => {};
|
||||
window.$RefreshSig$ = () => (type) => type;
|
||||
window.__vite_plugin_react_preamble_installed__ = true;
|
||||
}
|
||||
})
|
||||
.then(() => import(devEntryUrl))
|
||||
.then(() => waitForRootMount(4000))
|
||||
.then((mounted) => {
|
||||
if (!mounted) {
|
||||
throw new Error('Dev bundle loaded but app did not mount');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
attempt += 1;
|
||||
console.warn('[OpenChamber] VS Code webview dev bundle unavailable, retrying...', error);
|
||||
setStatus('Waiting for webview dev server (' + hostLabel + ')... attempt ' + attempt);
|
||||
window.setTimeout(() => {
|
||||
tryLoadDevBundle();
|
||||
}, retryDelayMs);
|
||||
});
|
||||
};
|
||||
|
||||
tryLoadDevBundle();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
root: path.resolve(__dirname, 'webview'),
|
||||
base: './', // Use relative paths for VS Code webview
|
||||
plugins: [
|
||||
@@ -27,11 +27,25 @@ export default defineConfig({
|
||||
format: 'es',
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'),
|
||||
'global': 'globalThis',
|
||||
'__OPENCHAMBER_WEBVIEW_BUILD_TIME__': JSON.stringify(new Date().toISOString()),
|
||||
},
|
||||
envPrefix: ['VITE_'],
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
protocol: 'ws',
|
||||
port: 5173,
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@opencode-ai/sdk/v2'],
|
||||
},
|
||||
@@ -48,4 +62,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -982,6 +982,27 @@ onCommand('addToContext', (payload) => {
|
||||
});
|
||||
});
|
||||
|
||||
onCommand('addFileMentions', (payload) => {
|
||||
const rawPaths = Array.isArray((payload as { paths?: unknown[] })?.paths)
|
||||
? (payload as { paths: unknown[] }).paths
|
||||
: [];
|
||||
const paths = rawPaths
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
|
||||
if (paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mentionText = paths.map((relativePath) => `@${relativePath}`).join(' ');
|
||||
|
||||
import('@/stores/useSessionStore').then(({ useSessionStore }) => {
|
||||
const store = useSessionStore.getState();
|
||||
store.setPendingInputText(mentionText, 'append-inline');
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for createSessionWithPrompt command from extension (Explain, Improve Code)
|
||||
onCommand('createSessionWithPrompt', (payload) => {
|
||||
const { prompt } = payload as { prompt: string };
|
||||
|
||||
Reference in New Issue
Block a user