Fix Windows path/spawn regressions and session visibility (#552)
* Update OpenCode CLI detection * fix(windows): handle cli file-url import and cmd shim spawn Fixes #533 and #521. * fix(windows): stabilize api path rewrite and session merge Fixes #548.
This commit is contained in:
committed by
GitHub
parent
122a45a890
commit
6eb5d1afa9
+317
-46
@@ -3398,6 +3398,15 @@ const ENV_EFFECTIVE_PORT = ENV_CONFIGURED_OPENCODE_HOST?.port ?? ENV_CONFIGURED_
|
||||
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
|
||||
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
||||
const ENV_DESKTOP_NOTIFY = process.env.OPENCHAMBER_DESKTOP_NOTIFY === 'true';
|
||||
const ENV_CONFIGURED_OPENCODE_WSL_DISTRO =
|
||||
typeof process.env.OPENCODE_WSL_DISTRO === 'string' && process.env.OPENCODE_WSL_DISTRO.trim().length > 0
|
||||
? process.env.OPENCODE_WSL_DISTRO.trim()
|
||||
: (
|
||||
typeof process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO === 'string' &&
|
||||
process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO.trim().length > 0
|
||||
? process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO.trim()
|
||||
: null
|
||||
);
|
||||
|
||||
// OpenCode server authentication (Basic Auth with username "opencode")
|
||||
|
||||
@@ -3644,6 +3653,10 @@ let resolvedOpencodeBinary = null;
|
||||
let resolvedOpencodeBinarySource = null;
|
||||
let resolvedNodeBinary = null;
|
||||
let resolvedBunBinary = null;
|
||||
let useWslForOpencode = false;
|
||||
let resolvedWslBinary = null;
|
||||
let resolvedWslOpencodePath = null;
|
||||
let resolvedWslDistro = null;
|
||||
|
||||
function isExecutable(filePath) {
|
||||
try {
|
||||
@@ -3682,6 +3695,136 @@ function searchPathFor(binaryName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isWslExecutableValue(value) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
return /(^|[\\/])wsl(\.exe)?$/i.test(trimmed);
|
||||
}
|
||||
|
||||
function clearWslOpencodeResolution() {
|
||||
useWslForOpencode = false;
|
||||
resolvedWslBinary = null;
|
||||
resolvedWslOpencodePath = null;
|
||||
resolvedWslDistro = null;
|
||||
}
|
||||
|
||||
function resolveWslExecutablePath() {
|
||||
if (process.platform !== 'win32') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicit = [process.env.WSL_BINARY, process.env.OPENCHAMBER_WSL_BINARY]
|
||||
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
for (const candidate of explicit) {
|
||||
if (isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = spawnSync('where', ['wsl'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
if (result.status === 0) {
|
||||
const lines = (result.stdout || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const found = lines.find((line) => isExecutable(line));
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
||||
const fallback = path.join(systemRoot, 'System32', 'wsl.exe');
|
||||
if (isExecutable(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildWslExecArgs(execArgs, distroOverride = null) {
|
||||
const distro = typeof distroOverride === 'string' && distroOverride.trim().length > 0
|
||||
? distroOverride.trim()
|
||||
: ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
|
||||
|
||||
const prefix = distro ? ['-d', distro] : [];
|
||||
return [...prefix, '--exec', ...execArgs];
|
||||
}
|
||||
|
||||
function probeWslForOpencode() {
|
||||
if (process.platform !== 'win32') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wslBinary = resolveWslExecutablePath();
|
||||
if (!wslBinary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = spawnSync(
|
||||
wslBinary,
|
||||
buildWslExecArgs(['sh', '-lc', 'command -v opencode']),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 6000,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = (result.stdout || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const found = lines[0] || '';
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
wslBinary,
|
||||
opencodePath: found,
|
||||
distro: ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyWslOpencodeResolution({ wslBinary, opencodePath, source = 'wsl', distro = null } = {}) {
|
||||
const resolvedWsl = wslBinary || resolveWslExecutablePath();
|
||||
if (!resolvedWsl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
useWslForOpencode = true;
|
||||
resolvedWslBinary = resolvedWsl;
|
||||
resolvedWslOpencodePath = typeof opencodePath === 'string' && opencodePath.trim().length > 0
|
||||
? opencodePath.trim()
|
||||
: 'opencode';
|
||||
resolvedWslDistro = typeof distro === 'string' && distro.trim().length > 0 ? distro.trim() : ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
|
||||
resolvedOpencodeBinary = `wsl:${resolvedWslOpencodePath}`;
|
||||
resolvedOpencodeBinarySource = source;
|
||||
|
||||
// Keep OPENCODE_BINARY empty in WSL mode to avoid native spawn attempts.
|
||||
delete process.env.OPENCODE_BINARY;
|
||||
return resolvedOpencodeBinary;
|
||||
}
|
||||
|
||||
function resolveOpencodeCliPath() {
|
||||
const explicit = [
|
||||
process.env.OPENCODE_BINARY,
|
||||
@@ -3694,6 +3837,7 @@ function resolveOpencodeCliPath() {
|
||||
|
||||
for (const candidate of explicit) {
|
||||
if (isExecutable(candidate)) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinarySource = 'env';
|
||||
return candidate;
|
||||
}
|
||||
@@ -3701,6 +3845,7 @@ function resolveOpencodeCliPath() {
|
||||
|
||||
const resolvedFromPath = searchPathFor('opencode');
|
||||
if (resolvedFromPath) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinarySource = 'path';
|
||||
return resolvedFromPath;
|
||||
}
|
||||
@@ -3739,6 +3884,7 @@ function resolveOpencodeCliPath() {
|
||||
const fallbacks = process.platform === 'win32' ? winFallbacks : unixFallbacks;
|
||||
for (const candidate of fallbacks) {
|
||||
if (isExecutable(candidate)) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinarySource = 'fallback';
|
||||
return candidate;
|
||||
}
|
||||
@@ -3757,6 +3903,7 @@ function resolveOpencodeCliPath() {
|
||||
.filter(Boolean);
|
||||
const found = lines.find((line) => isExecutable(line));
|
||||
if (found) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinarySource = 'where';
|
||||
return found;
|
||||
}
|
||||
@@ -3764,6 +3911,15 @@ function resolveOpencodeCliPath() {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const wsl = probeWslForOpencode();
|
||||
if (wsl) {
|
||||
return applyWslOpencodeResolution({
|
||||
wslBinary: wsl.wslBinary,
|
||||
opencodePath: wsl.opencodePath,
|
||||
source: 'wsl',
|
||||
distro: wsl.distro,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3778,6 +3934,7 @@ function resolveOpencodeCliPath() {
|
||||
if (result.status === 0) {
|
||||
const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
|
||||
if (found && isExecutable(found)) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinarySource = 'shell';
|
||||
return found;
|
||||
}
|
||||
@@ -4059,10 +4216,44 @@ async function applyOpencodeBinaryFromSettings() {
|
||||
delete process.env.OPENCODE_BINARY;
|
||||
resolvedOpencodeBinary = null;
|
||||
resolvedOpencodeBinarySource = null;
|
||||
clearWslOpencodeResolution();
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = typeof settings.opencodeBinary === 'string' ? settings.opencodeBinary.trim() : '';
|
||||
|
||||
const explicitWslPath = process.platform === 'win32' && typeof raw === 'string'
|
||||
? raw.match(/^wsl:\s*(.+)$/i)
|
||||
: null;
|
||||
|
||||
if (explicitWslPath && explicitWslPath[1] && explicitWslPath[1].trim().length > 0) {
|
||||
const probe = probeWslForOpencode();
|
||||
const applied = applyWslOpencodeResolution({
|
||||
wslBinary: probe?.wslBinary || resolveWslExecutablePath(),
|
||||
opencodePath: explicitWslPath[1].trim(),
|
||||
source: 'settings-wsl-path',
|
||||
distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
||||
});
|
||||
if (applied) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && (isWslExecutableValue(raw) || isWslExecutableValue(normalized || ''))) {
|
||||
const probe = probeWslForOpencode();
|
||||
const applied = applyWslOpencodeResolution({
|
||||
wslBinary: probe?.wslBinary || normalized || raw || null,
|
||||
opencodePath: probe?.opencodePath || 'opencode',
|
||||
source: 'settings-wsl',
|
||||
distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
||||
});
|
||||
if (applied) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized && isExecutable(normalized)) {
|
||||
clearWslOpencodeResolution();
|
||||
process.env.OPENCODE_BINARY = normalized;
|
||||
prependToPath(path.dirname(normalized));
|
||||
resolvedOpencodeBinary = normalized;
|
||||
@@ -4071,7 +4262,6 @@ async function applyOpencodeBinaryFromSettings() {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const raw = typeof settings.opencodeBinary === 'string' ? settings.opencodeBinary.trim() : '';
|
||||
if (raw) {
|
||||
console.warn(`Configured settings.opencodeBinary is not executable: ${raw}`);
|
||||
}
|
||||
@@ -4084,12 +4274,16 @@ async function applyOpencodeBinaryFromSettings() {
|
||||
|
||||
function ensureOpencodeCliEnv() {
|
||||
if (resolvedOpencodeBinary) {
|
||||
if (useWslForOpencode) {
|
||||
return resolvedOpencodeBinary;
|
||||
}
|
||||
ensureOpencodeShimRuntime(resolvedOpencodeBinary);
|
||||
return resolvedOpencodeBinary;
|
||||
}
|
||||
|
||||
const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
|
||||
if (existing && isExecutable(existing)) {
|
||||
clearWslOpencodeResolution();
|
||||
resolvedOpencodeBinary = existing;
|
||||
resolvedOpencodeBinarySource = resolvedOpencodeBinarySource || 'env';
|
||||
prependToPath(path.dirname(existing));
|
||||
@@ -4099,6 +4293,13 @@ function ensureOpencodeCliEnv() {
|
||||
|
||||
const resolved = resolveOpencodeCliPath();
|
||||
if (resolved) {
|
||||
if (useWslForOpencode) {
|
||||
resolvedOpencodeBinary = resolved;
|
||||
resolvedOpencodeBinarySource = resolvedOpencodeBinarySource || 'wsl';
|
||||
console.log(`Resolved opencode CLI via WSL: ${resolvedWslOpencodePath || 'opencode'}`);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
process.env.OPENCODE_BINARY = resolved;
|
||||
prependToPath(path.dirname(resolved));
|
||||
ensureOpencodeShimRuntime(resolved);
|
||||
@@ -4108,6 +4309,7 @@ function ensureOpencodeCliEnv() {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
clearWslOpencodeResolution();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5171,12 +5373,34 @@ async function createManagedOpenCodeServerProcess({
|
||||
env,
|
||||
}) {
|
||||
let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
|
||||
const args = ['serve', '--hostname', hostname, '--port', String(port)];
|
||||
let args = ['serve', '--hostname', hostname, '--port', String(port)];
|
||||
|
||||
if (process.platform === 'win32' && useWslForOpencode) {
|
||||
const wslBinary = resolvedWslBinary || resolveWslExecutablePath();
|
||||
if (!wslBinary) {
|
||||
throw new Error('WSL executable not found while attempting to launch OpenCode from WSL');
|
||||
}
|
||||
|
||||
const wslOpencode = resolvedWslOpencodePath && resolvedWslOpencodePath.trim().length > 0
|
||||
? resolvedWslOpencodePath.trim()
|
||||
: 'opencode';
|
||||
const serveHost = hostname === '127.0.0.1' ? '0.0.0.0' : hostname;
|
||||
|
||||
binary = wslBinary;
|
||||
args = buildWslExecArgs([
|
||||
wslOpencode,
|
||||
'serve',
|
||||
'--hostname',
|
||||
serveHost,
|
||||
'--port',
|
||||
String(port),
|
||||
], resolvedWslDistro);
|
||||
}
|
||||
|
||||
// On Windows, Bun/Node cannot directly spawn shell wrapper scripts (#!/bin/sh).
|
||||
// Detect if the resolved binary is a shim that wraps a Node/Bun script and
|
||||
// resolve the actual target so we can spawn it with the correct interpreter.
|
||||
if (process.platform === 'win32') {
|
||||
if (process.platform === 'win32' && !useWslForOpencode) {
|
||||
const interpreter = opencodeShimInterpreter(binary);
|
||||
if (interpreter) {
|
||||
// Binary itself has a node/bun shebang – spawn via that interpreter.
|
||||
@@ -5675,29 +5899,53 @@ function setupProxy(app) {
|
||||
}
|
||||
app.set('opencodeProxyConfigured', true);
|
||||
|
||||
// Windows path normalization: OpenCode CLI stores paths with backslashes in the DB,
|
||||
// but the frontend sends forward slashes. Rewrite directory query params on Windows.
|
||||
// Must run BEFORE all other /api middleware.
|
||||
if (process.platform === 'win32') {
|
||||
app.use('/api', (req, _res, next) => {
|
||||
// Parse directory from the raw URL since Express query parsing may not be available
|
||||
const rawUrl = req.originalUrl || req.url || '';
|
||||
const dirMatch = rawUrl.match(/[?&]directory=([^&]*)/);
|
||||
if (dirMatch) {
|
||||
const decoded = decodeURIComponent(dirMatch[1]);
|
||||
if (decoded.includes('/')) {
|
||||
const fixed = decoded.replace(/\//g, '\\');
|
||||
const fixedEncoded = encodeURIComponent(fixed);
|
||||
const newUrl = rawUrl.replace(/([?&]directory=)[^&]*/, '$1' + fixedEncoded);
|
||||
console.log(`[Win32PathFix] Rewrote directory: "${decoded}" → "${fixed}"`);
|
||||
console.log(`[Win32PathFix] URL: "${rawUrl}" → "${newUrl}"`);
|
||||
req.originalUrl = newUrl;
|
||||
req.url = newUrl;
|
||||
}
|
||||
const stripApiPrefix = (rawUrl) => {
|
||||
if (typeof rawUrl !== 'string' || !rawUrl) {
|
||||
return '/';
|
||||
}
|
||||
if (rawUrl === '/api') {
|
||||
return '/';
|
||||
}
|
||||
if (rawUrl.startsWith('/api/')) {
|
||||
return rawUrl.slice(4);
|
||||
}
|
||||
return rawUrl;
|
||||
};
|
||||
|
||||
// Keep route matching stable; only rewrite the proxied upstream path.
|
||||
const rewriteWindowsDirectoryParam = (upstreamPath) => {
|
||||
if (process.platform !== 'win32') {
|
||||
return upstreamPath;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(upstreamPath, 'http://openchamber.local');
|
||||
const pathname = parsed.pathname || '/';
|
||||
if (pathname === '/session' || pathname.startsWith('/session/')) {
|
||||
return upstreamPath;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
const directory = parsed.searchParams.get('directory');
|
||||
if (!directory || !directory.includes('/')) {
|
||||
return upstreamPath;
|
||||
}
|
||||
const fixed = directory.replace(/\//g, '\\');
|
||||
parsed.searchParams.set('directory', fixed);
|
||||
const rewritten = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
||||
if (rewritten !== upstreamPath) {
|
||||
console.log(`[Win32PathFix] Rewrote directory: "${directory}" → "${fixed}"`);
|
||||
console.log(`[Win32PathFix] URL: "${upstreamPath}" → "${rewritten}"`);
|
||||
}
|
||||
return rewritten;
|
||||
} catch {
|
||||
return upstreamPath;
|
||||
}
|
||||
};
|
||||
|
||||
const getUpstreamPathForRequest = (req) => {
|
||||
const rawUrl = (typeof req.originalUrl === 'string' && req.originalUrl)
|
||||
? req.originalUrl
|
||||
: (typeof req.url === 'string' ? req.url : '/');
|
||||
return rewriteWindowsDirectoryParam(stripApiPrefix(rawUrl));
|
||||
};
|
||||
|
||||
app.use('/api', (req, res, next) => {
|
||||
if (
|
||||
@@ -5733,7 +5981,7 @@ function setupProxy(app) {
|
||||
|
||||
const forwardSseRequest = async (req, res) => {
|
||||
const startedAt = Date.now();
|
||||
const upstreamPath = req.originalUrl.replace(/^\/api/, '');
|
||||
const upstreamPath = getUpstreamPathForRequest(req);
|
||||
const targetUrl = buildOpenCodeUrl(upstreamPath, '');
|
||||
const authHeaders = getOpenCodeAuthHeaders();
|
||||
|
||||
@@ -5970,7 +6218,7 @@ function setupProxy(app) {
|
||||
|
||||
const forwardGenericApiRequest = async (req, res) => {
|
||||
try {
|
||||
const upstreamPath = req.originalUrl.replace(/^\/api/, '');
|
||||
const upstreamPath = getUpstreamPathForRequest(req);
|
||||
const targetUrl = buildOpenCodeUrl(upstreamPath, '');
|
||||
const headers = collectForwardHeaders(req);
|
||||
const method = String(req.method || 'GET').toUpperCase();
|
||||
@@ -6008,7 +6256,7 @@ function setupProxy(app) {
|
||||
// This avoids edge-cases in generic proxy streaming for multi-file attachments.
|
||||
app.post('/api/session/:sessionId/message', express.raw({ type: '*/*', limit: '50mb' }), async (req, res) => {
|
||||
try {
|
||||
const upstreamPath = req.originalUrl.replace(/^\/api/, '');
|
||||
const upstreamPath = getUpstreamPathForRequest(req);
|
||||
const targetUrl = buildOpenCodeUrl(upstreamPath, '');
|
||||
const authHeaders = getOpenCodeAuthHeaders();
|
||||
|
||||
@@ -6067,7 +6315,8 @@ function setupProxy(app) {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
};
|
||||
const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
|
||||
const globalSessions = globalRes.ok ? (await globalRes.json()) : [];
|
||||
const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : [];
|
||||
const globalSessions = Array.isArray(globalPayload) ? globalPayload : [];
|
||||
|
||||
const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
||||
let projectDirs = [];
|
||||
@@ -6075,33 +6324,47 @@ function setupProxy(app) {
|
||||
const settingsRaw = fs.readFileSync(settingsPath, 'utf8');
|
||||
const settings = JSON.parse(settingsRaw);
|
||||
projectDirs = (settings.projects || [])
|
||||
.map(p => p.path)
|
||||
.filter(p => typeof p === 'string' && p.length > 0);
|
||||
.map((project) => (typeof project?.path === 'string' ? project.path.trim() : ''))
|
||||
.filter(Boolean);
|
||||
} catch {}
|
||||
|
||||
const seen = new Set(globalSessions.map(s => s.id));
|
||||
const seen = new Set(
|
||||
globalSessions
|
||||
.map((session) => (session && typeof session.id === 'string' ? session.id : null))
|
||||
.filter((id) => typeof id === 'string')
|
||||
);
|
||||
const extraSessions = [];
|
||||
for (const dir of projectDirs) {
|
||||
const backslashDir = dir.replace(/\//g, '\\');
|
||||
const encoded = encodeURIComponent(backslashDir);
|
||||
try {
|
||||
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
|
||||
if (dirRes.ok) {
|
||||
const dirSessions = await dirRes.json();
|
||||
if (Array.isArray(dirSessions)) {
|
||||
for (const s of dirSessions) {
|
||||
if (s && s.id && !seen.has(s.id)) {
|
||||
seen.add(s.id);
|
||||
extraSessions.push(s);
|
||||
const candidates = Array.from(new Set([
|
||||
dir,
|
||||
dir.replace(/\\/g, '/'),
|
||||
dir.replace(/\//g, '\\'),
|
||||
]));
|
||||
for (const candidateDir of candidates) {
|
||||
const encoded = encodeURIComponent(candidateDir);
|
||||
try {
|
||||
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
|
||||
if (dirRes.ok) {
|
||||
const dirPayload = await dirRes.json().catch(() => []);
|
||||
const dirSessions = Array.isArray(dirPayload) ? dirPayload : [];
|
||||
for (const session of dirSessions) {
|
||||
const id = session && typeof session.id === 'string' ? session.id : null;
|
||||
if (id && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
extraSessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const merged = [...globalSessions, ...extraSessions];
|
||||
merged.sort((a, b) => (b.time_updated || 0) - (a.time_updated || 0));
|
||||
merged.sort((a, b) => {
|
||||
const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
|
||||
const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`);
|
||||
return res.json(merged);
|
||||
} catch (error) {
|
||||
@@ -6310,6 +6573,10 @@ async function main(options = {}) {
|
||||
opencodeBinaryResolved: resolvedOpencodeBinary || null,
|
||||
opencodeBinarySource: resolvedOpencodeBinarySource || null,
|
||||
opencodeShimInterpreter: resolvedOpencodeBinary ? opencodeShimInterpreter(resolvedOpencodeBinary) : null,
|
||||
opencodeViaWsl: useWslForOpencode,
|
||||
opencodeWslBinary: resolvedWslBinary || null,
|
||||
opencodeWslPath: resolvedWslOpencodePath || null,
|
||||
opencodeWslDistro: resolvedWslDistro || null,
|
||||
nodeBinaryResolved: resolvedNodeBinary || null,
|
||||
bunBinaryResolved: resolvedBunBinary || null,
|
||||
});
|
||||
@@ -7687,6 +7954,10 @@ async function main(options = {}) {
|
||||
detectedNow,
|
||||
detectedSourceNow,
|
||||
shim,
|
||||
viaWsl: useWslForOpencode,
|
||||
wslBinary: resolvedWslBinary || null,
|
||||
wslPath: resolvedWslOpencodePath || null,
|
||||
wslDistro: resolvedWslDistro || null,
|
||||
node: resolvedNodeBinary || null,
|
||||
bun: resolvedBunBinary || null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user