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:
Bohdan Triapitsyn
2026-03-23 23:51:55 +02:00
committed by GitHub
parent ea6d4c4d43
commit 1231fd773e
39 changed files with 1441 additions and 791 deletions
+132 -2
View File
@@ -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>`;
}