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
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const extensionPath = path.join(repoRoot, 'packages', 'vscode');
|
||||
const useDetachedChildren = process.platform === 'darwin';
|
||||
|
||||
const codeBin = process.env.OPENCHAMBER_VSCODE_BIN || 'code';
|
||||
const workspaceArg = process.argv[2] || process.env.OPENCHAMBER_VSCODE_DEV_WORKSPACE || repoRoot;
|
||||
const workspacePath = path.resolve(workspaceArg);
|
||||
|
||||
const resolveDevServerAddress = () => {
|
||||
const configured = process.env.OPENCHAMBER_VSCODE_WEBVIEW_URL;
|
||||
if (!configured) {
|
||||
return { host: 'localhost', port: 5173 };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(configured);
|
||||
return {
|
||||
host: parsed.hostname || '127.0.0.1',
|
||||
port: Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80),
|
||||
};
|
||||
} catch {
|
||||
return { host: 'localhost', port: 5173 };
|
||||
}
|
||||
};
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const probePort = (host, port, timeoutMs = 500) => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ host, port });
|
||||
let settled = false;
|
||||
|
||||
const done = (ok) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
socket.destroy();
|
||||
resolve(ok);
|
||||
};
|
||||
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.once('connect', () => done(true));
|
||||
socket.once('timeout', () => done(false));
|
||||
socket.once('error', () => done(false));
|
||||
});
|
||||
};
|
||||
|
||||
const waitForPort = async (host, port, timeoutMs, shouldAbort) => {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (shouldAbort()) return false;
|
||||
const ready = await probePort(host, port);
|
||||
if (ready) return true;
|
||||
await sleep(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
console.error(`[dev:vscode] Workspace path not found: ${workspacePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run(label, command, args, options = {}) {
|
||||
const child = spawn(command, args, {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env },
|
||||
detached: useDetachedChildren,
|
||||
...options,
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`[dev:vscode] Failed to start ${label}:`, error);
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
function waitForExit(child, timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const onExit = () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
child.off('exit', onExit);
|
||||
resolve();
|
||||
}, timeoutMs);
|
||||
|
||||
child.once('exit', onExit);
|
||||
});
|
||||
}
|
||||
|
||||
function signalChild(child, signal) {
|
||||
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (useDetachedChildren && process.platform !== 'win32') {
|
||||
process.kill(-child.pid, signal);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopChildTree(child) {
|
||||
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
signalChild(child, 'SIGINT');
|
||||
await waitForExit(child, 2500);
|
||||
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
signalChild(child, 'SIGTERM');
|
||||
await waitForExit(child, 2500);
|
||||
}
|
||||
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
signalChild(child, 'SIGKILL');
|
||||
await waitForExit(child, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
let shuttingDown = false;
|
||||
const dev = run('vscode dev watchers', 'bun', ['run', '--cwd', 'packages/vscode', 'dev']);
|
||||
|
||||
console.log(`[dev:vscode] Starting extension host with ${codeBin}`);
|
||||
console.log(`[dev:vscode] Workspace: ${workspacePath}`);
|
||||
console.log(`[dev:vscode] Extension: ${extensionPath}`);
|
||||
const { host: devServerHost, port: devServerPort } = resolveDevServerAddress();
|
||||
console.log(`[dev:vscode] Waiting for webview dev server at ${devServerHost}:${devServerPort}`);
|
||||
|
||||
const ready = await waitForPort(devServerHost, devServerPort, 30000, () => shuttingDown || dev.exitCode !== null || dev.signalCode !== null);
|
||||
if (!ready) {
|
||||
console.warn('[dev:vscode] Webview dev server not ready in time, opening extension host anyway');
|
||||
}
|
||||
|
||||
const host = run(
|
||||
'vscode extension host',
|
||||
codeBin,
|
||||
[
|
||||
'--new-window',
|
||||
'--disable-extensions',
|
||||
'--extensionDevelopmentPath',
|
||||
extensionPath,
|
||||
'--wait',
|
||||
workspacePath,
|
||||
],
|
||||
{ detached: false },
|
||||
);
|
||||
|
||||
async function shutdown(exitCode = 0) {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
shuttingDown = true;
|
||||
await Promise.all([stopChildTree(host), stopChildTree(dev)]);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function onChildExit(label) {
|
||||
return (code, signal) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code !== 0 || signal) {
|
||||
console.error(`[dev:vscode] ${label} exited unexpectedly (code=${code ?? 'null'} signal=${signal ?? 'none'})`);
|
||||
shutdown(typeof code === 'number' ? code : 1).catch(() => process.exit(1));
|
||||
return;
|
||||
}
|
||||
|
||||
shutdown(0).catch(() => process.exit(1));
|
||||
};
|
||||
}
|
||||
|
||||
dev.on('exit', onChildExit('watchers'));
|
||||
host.on('exit', onChildExit('extension host'));
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
shutdown(130).catch(() => process.exit(130));
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
shutdown(143).catch(() => process.exit(143));
|
||||
});
|
||||
|
||||
process.on('SIGHUP', () => {
|
||||
shutdown(129).catch(() => process.exit(129));
|
||||
});
|
||||
Reference in New Issue
Block a user