Fix subagent crash and add external OpenCode server support (#188)
* 🐛 Fix UI crash when subagent is active Remove sessions dependency from hooks to prevent cascading re-renders. Use getState() instead and switch to getGlobalSessionStatus(). * ✨ Add support for connecting to external OpenCode server Add OPENCODE_SKIP_START env var to skip starting embedded server. Use OPENCODE_PORT to connect to existing OpenCode instance. Update help text to document the new environment variables. * 📝 Document external OpenCode server support Add OPENCODE_PORT and OPENCODE_SKIP_START to READMEs. Update AGENTS.md with external server integration notes. * ✨ Add URL-based routing for shareable session links - Add react-router-dom dependency - Create URL store for bi-directional sync with Zustand - Add WebRouter/DesktopRouter context for runtime-aware routing - Add useURLSync hook to sync URL with session/tab state - Add useNavigation hook with copySessionLink utility - Add share button in Header for copying session links - Update App.tsx to use router wrappers URL structure: /session/:sessionId?tab={chat|git|diff|terminal|files}&directory=/path /settings This enables shareable links and deep-linking to specific sessions. * Revert "✨ Add URL-based routing for shareable session links" This reverts commit b53ee304950a789538fa3d4a236d6361e634ea61.
This commit is contained in:
@@ -41,6 +41,7 @@ All scripts are in `package.json`.
|
|||||||
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
|
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
|
||||||
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
|
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
|
||||||
- Web runtime filesystem endpoints: search `packages/web/server/index.js` for `/api/fs/`
|
- Web runtime filesystem endpoints: search `packages/web/server/index.js` for `/api/fs/`
|
||||||
|
- External server support: Set `OPENCODE_PORT` and `OPENCODE_SKIP_START=true` to connect to existing OpenCode instance
|
||||||
|
|
||||||
## Key UI patterns (reference files)
|
## Key UI patterns (reference files)
|
||||||
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
|
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ openchamber --ui-password secret # Password-protect UI
|
|||||||
openchamber --try-cf-tunnel # Create a Cloudflare Quick Tunnel for remote access
|
openchamber --try-cf-tunnel # Create a Cloudflare Quick Tunnel for remote access
|
||||||
openchamber --try-cf-tunnel --tunnel-qr # Show QR code for easy mobile access
|
openchamber --try-cf-tunnel --tunnel-qr # Show QR code for easy mobile access
|
||||||
openchamber --try-cf-tunnel --tunnel-password-url # Include password in URL for auto-login
|
openchamber --try-cf-tunnel --tunnel-password-url # Include password in URL for auto-login
|
||||||
|
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||||
openchamber stop # Stop server
|
openchamber stop # Stop server
|
||||||
openchamber update # Update to latest version
|
openchamber update # Update to latest version
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export const useEventStream = () => {
|
|||||||
dismissQuestion,
|
dismissQuestion,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
applySessionMetadata,
|
applySessionMetadata,
|
||||||
sessions,
|
|
||||||
getWorktreeMetadata,
|
getWorktreeMetadata,
|
||||||
loadMessages,
|
loadMessages,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
@@ -152,13 +151,15 @@ export const useEventStream = () => {
|
|||||||
console.warn('Failed to inspect worktree metadata for session directory:', error);
|
console.warn('Failed to inspect worktree metadata for session directory:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionRecord = sessions.find((entry) => entry.id === currentSessionId);
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
const sessionRecord = currentSessions.find((entry) => entry.id === currentSessionId);
|
||||||
if (sessionRecord && typeof sessionRecord.directory === 'string' && sessionRecord.directory.trim().length > 0) {
|
if (sessionRecord && typeof sessionRecord.directory === 'string' && sessionRecord.directory.trim().length > 0) {
|
||||||
return sessionRecord.directory.trim();
|
return sessionRecord.directory.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [currentSessionId, getWorktreeMetadata, sessions]);
|
}, [currentSessionId, getWorktreeMetadata]);
|
||||||
|
|
||||||
const effectiveDirectory = React.useMemo(() => {
|
const effectiveDirectory = React.useMemo(() => {
|
||||||
if (activeSessionDirectory && activeSessionDirectory.length > 0) {
|
if (activeSessionDirectory && activeSessionDirectory.length > 0) {
|
||||||
@@ -180,9 +181,9 @@ export const useEventStream = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pending.forEach((request) => {
|
for (const request of pending) {
|
||||||
addPermission(request as unknown as PermissionRequest);
|
addPermission(request as unknown as PermissionRequest);
|
||||||
});
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
@@ -192,7 +193,9 @@ export const useEventStream = () => {
|
|||||||
try {
|
try {
|
||||||
const projects = useProjectsStore.getState().projects;
|
const projects = useProjectsStore.getState().projects;
|
||||||
const projectDirs = projects.map((project) => project.path);
|
const projectDirs = projects.map((project) => project.path);
|
||||||
const sessionDirs = sessions.map((session) => (session as { directory?: string | null }).directory);
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
const sessionDirs = currentSessions.map((session) => (session as { directory?: string | null }).directory);
|
||||||
|
|
||||||
const directories = [effectiveDirectory, ...projectDirs, ...sessionDirs];
|
const directories = [effectiveDirectory, ...projectDirs, ...sessionDirs];
|
||||||
|
|
||||||
@@ -201,9 +204,9 @@ export const useEventStream = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pending.forEach((request) => {
|
for (const request of pending) {
|
||||||
addQuestion(request as unknown as QuestionRequest);
|
addQuestion(request as unknown as QuestionRequest);
|
||||||
});
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
@@ -215,7 +218,7 @@ export const useEventStream = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [addPermission, addQuestion, effectiveDirectory, sessions]);
|
}, [addPermission, addQuestion, effectiveDirectory]);
|
||||||
|
|
||||||
const normalizeDirectory = React.useCallback((value: string | null | undefined): string | null => {
|
const normalizeDirectory = React.useCallback((value: string | null | undefined): string | null => {
|
||||||
if (typeof value !== 'string') return null;
|
if (typeof value !== 'string') return null;
|
||||||
@@ -236,10 +239,12 @@ export const useEventStream = () => {
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = sessions.find((entry) => entry.id === sessionId);
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
const record = currentSessions.find((entry) => entry.id === sessionId);
|
||||||
return normalizeDirectory((record as { directory?: string | null })?.directory ?? null);
|
return normalizeDirectory((record as { directory?: string | null })?.directory ?? null);
|
||||||
},
|
},
|
||||||
[getWorktreeMetadata, normalizeDirectory, sessions]
|
[getWorktreeMetadata, normalizeDirectory]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setEventStreamStatus = useUIStore((state) => state.setEventStreamStatus);
|
const setEventStreamStatus = useUIStore((state) => state.setEventStreamStatus);
|
||||||
@@ -417,7 +422,9 @@ export const useEventStream = () => {
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionRecord = sessions.find((entry) => entry.id === id) as Session & { directory?: string | null };
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
const sessionRecord = currentSessions.find((entry) => entry.id === id) as Session & { directory?: string | null };
|
||||||
if (sessionRecord && typeof sessionRecord.directory === 'string' && sessionRecord.directory.trim().length > 0) {
|
if (sessionRecord && typeof sessionRecord.directory === 'string' && sessionRecord.directory.trim().length > 0) {
|
||||||
return sessionRecord.directory.trim();
|
return sessionRecord.directory.trim();
|
||||||
}
|
}
|
||||||
@@ -449,7 +456,7 @@ export const useEventStream = () => {
|
|||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
[applySessionMetadata, getWorktreeMetadata, sessions]
|
[applySessionMetadata, getWorktreeMetadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -498,15 +505,17 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
const applyStatusMap = (statusMap: Record<string, { type?: string }>) => {
|
const applyStatusMap = (statusMap: Record<string, { type?: string }>) => {
|
||||||
const observed = new Set<string>();
|
const observed = new Set<string>();
|
||||||
const knownSessionIds = new Set(sessions.map((session) => session.id));
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
const knownSessionIds = new Set(currentSessions.map((session) => session.id));
|
||||||
|
|
||||||
Object.entries(statusMap).forEach(([sessionId, raw]) => {
|
for (const [sessionId, raw] of Object.entries(statusMap)) {
|
||||||
if (!sessionId || !raw) return;
|
if (!sessionId || !raw) continue;
|
||||||
observed.add(sessionId);
|
observed.add(sessionId);
|
||||||
const phase: 'idle' | 'busy' =
|
const phase: 'idle' | 'busy' =
|
||||||
raw.type === 'busy' || raw.type === 'retry' ? 'busy' : 'idle';
|
raw.type === 'busy' || raw.type === 'retry' ? 'busy' : 'idle';
|
||||||
updateSessionActivityPhase(sessionId, phase);
|
updateSessionActivityPhase(sessionId, phase);
|
||||||
});
|
}
|
||||||
|
|
||||||
// OpenCode's /session/status may omit idle sessions (returns only busy/retry).
|
// OpenCode's /session/status may omit idle sessions (returns only busy/retry).
|
||||||
// Treat missing entries as idle to avoid sessions getting stuck "working".
|
// Treat missing entries as idle to avoid sessions getting stuck "working".
|
||||||
@@ -530,10 +539,12 @@ export const useEventStream = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directories = new Set<string>();
|
const directories = new Set<string>();
|
||||||
sessions.forEach((session) => {
|
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||||
|
const currentSessions = useSessionStore.getState().sessions;
|
||||||
|
for (const session of currentSessions) {
|
||||||
const directory = resolveSessionDirectoryForStatus(session.id);
|
const directory = resolveSessionDirectoryForStatus(session.id);
|
||||||
if (directory) directories.add(directory);
|
if (directory) directories.add(directory);
|
||||||
});
|
}
|
||||||
|
|
||||||
const effective = normalizeDirectory(effectiveDirectory ?? null);
|
const effective = normalizeDirectory(effectiveDirectory ?? null);
|
||||||
if (effective) directories.add(effective);
|
if (effective) directories.add(effective);
|
||||||
@@ -553,10 +564,10 @@ export const useEventStream = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const merged: Record<string, { type?: string }> = {};
|
const merged: Record<string, { type?: string }> = {};
|
||||||
results.forEach((result) => {
|
for (const result of results) {
|
||||||
if (result.status !== 'fulfilled' || !result.value) return;
|
if (result.status !== 'fulfilled' || !result.value) continue;
|
||||||
Object.assign(merged, result.value);
|
Object.assign(merged, result.value);
|
||||||
});
|
}
|
||||||
|
|
||||||
if (Object.keys(merged).length === 0) {
|
if (Object.keys(merged).length === 0) {
|
||||||
const hasActivePhases = Array.from(useSessionStore.getState().sessionActivityPhase?.values?.() ?? []).some(
|
const hasActivePhases = Array.from(useSessionStore.getState().sessionActivityPhase?.values?.() ?? []).some(
|
||||||
@@ -581,7 +592,7 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
sessionStatusRefreshInFlightRef.current = task;
|
sessionStatusRefreshInFlightRef.current = task;
|
||||||
return task;
|
return task;
|
||||||
}, [effectiveDirectory, normalizeDirectory, resolveSessionDirectoryForStatus, sessions, updateSessionActivityPhase]);
|
}, [effectiveDirectory, normalizeDirectory, resolveSessionDirectoryForStatus, updateSessionActivityPhase]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const nextSessionId = currentSessionId ?? null;
|
const nextSessionId = currentSessionId ?? null;
|
||||||
@@ -1452,13 +1463,10 @@ export const useEventStream = () => {
|
|||||||
lastEventTimestampRef.current = Date.now();
|
lastEventTimestampRef.current = Date.now();
|
||||||
publishStatus('connected', null);
|
publishStatus('connected', null);
|
||||||
checkConnection();
|
checkConnection();
|
||||||
|
|
||||||
const hasBusySessions = Array.from(useSessionStore.getState().sessionActivityPhase?.values?.() ?? []).some(
|
// Always refresh session activity status on connect to detect any
|
||||||
(phase) => phase === 'busy' || phase === 'cooldown'
|
// already-running sessions (e.g., started via CLI before UI opened)
|
||||||
);
|
void refreshSessionActivityStatus();
|
||||||
if (hasBusySessions) {
|
|
||||||
void refreshSessionActivityStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
void bootstrapState('sse_reconnected');
|
void bootstrapState('sse_reconnected');
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const useSessionStatusBootstrap = () => {
|
|||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
try {
|
try {
|
||||||
const statusMap = await opencodeClient.getSessionStatus();
|
// Use global status to detect busy sessions across all directories,
|
||||||
|
// including sessions started externally (e.g., via CLI) before UI opened
|
||||||
|
const statusMap = await opencodeClient.getGlobalSessionStatus();
|
||||||
if (cancelled || !statusMap) return;
|
if (cancelled || !statusMap) return;
|
||||||
|
|
||||||
const phases = new Map<string, 'idle' | 'busy' | 'cooldown'>();
|
const phases = new Map<string, 'idle' | 'busy' | 'cooldown'>();
|
||||||
|
|||||||
@@ -31,10 +31,16 @@ openchamber # Start on port 3000
|
|||||||
openchamber --port 8080 # Custom port
|
openchamber --port 8080 # Custom port
|
||||||
openchamber --daemon # Background mode
|
openchamber --daemon # Background mode
|
||||||
openchamber --ui-password secret # Password-protect UI
|
openchamber --ui-password secret # Password-protect UI
|
||||||
|
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
|
||||||
openchamber stop # Stop server
|
openchamber stop # Stop server
|
||||||
openchamber update # Update to latest version
|
openchamber update # Update to latest version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `OPENCODE_PORT` - Port of external OpenCode server to connect to (instead of starting embedded server)
|
||||||
|
- `OPENCODE_SKIP_START` - Skip starting embedded OpenCode server (use with `OPENCODE_PORT` to connect to external instance)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- [OpenCode CLI](https://opencode.ai) installed (`opencode`)
|
- [OpenCode CLI](https://opencode.ai) installed (`opencode`)
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function parseArgs() {
|
|||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
OpenChamber - Web interface for the OpenCode AI coding agent
|
OpenChamber - Web interface for the OpenCode AI coding agent
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
openchamber [COMMAND] [OPTIONS]
|
openchamber [COMMAND] [OPTIONS]
|
||||||
@@ -176,7 +176,9 @@ OPTIONS:
|
|||||||
-v, --version Show version
|
-v, --version Show version
|
||||||
|
|
||||||
ENVIRONMENT:
|
ENVIRONMENT:
|
||||||
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
||||||
|
OPENCODE_PORT Port of external OpenCode server to connect to
|
||||||
|
OPENCODE_SKIP_START Skip starting OpenCode, use external server
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
openchamber # Start on default port 3000 (or a free port)
|
openchamber # Start on default port 3000 (or a free port)
|
||||||
@@ -506,6 +508,7 @@ const commands = {
|
|||||||
OPENCODE_BINARY: opencodeBinary,
|
OPENCODE_BINARY: opencodeBinary,
|
||||||
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
||||||
OPENCHAMBER_TRY_CF_TUNNEL: options.tryCfTunnel ? 'true' : 'false',
|
OPENCHAMBER_TRY_CF_TUNNEL: options.tryCfTunnel ? 'true' : 'false',
|
||||||
|
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -574,6 +577,9 @@ const commands = {
|
|||||||
if (typeof effectiveUiPassword === 'string') {
|
if (typeof effectiveUiPassword === 'string') {
|
||||||
process.env.OPENCHAMBER_UI_PASSWORD = effectiveUiPassword;
|
process.env.OPENCHAMBER_UI_PASSWORD = effectiveUiPassword;
|
||||||
}
|
}
|
||||||
|
if (process.env.OPENCODE_SKIP_START) {
|
||||||
|
process.env.OPENCHAMBER_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START;
|
||||||
|
}
|
||||||
if (showAutoGeneratedPassword) {
|
if (showAutoGeneratedPassword) {
|
||||||
console.log(`\n🔐 Auto-generated password: \x1b[92m${effectiveUiPassword}\x1b[0m`);
|
console.log(`\n🔐 Auto-generated password: \x1b[92m${effectiveUiPassword}\x1b[0m`);
|
||||||
console.log('⚠️ Save this password - it won\'t be shown again!\n');
|
console.log('⚠️ Save this password - it won\'t be shown again!\n');
|
||||||
@@ -590,6 +596,7 @@ const commands = {
|
|||||||
OPENCODE_BINARY: opencodeBinary,
|
OPENCODE_BINARY: opencodeBinary,
|
||||||
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
||||||
OPENCHAMBER_TRY_CF_TUNNEL: options.tryCfTunnel ? 'true' : 'false',
|
OPENCHAMBER_TRY_CF_TUNNEL: options.tryCfTunnel ? 'true' : 'false',
|
||||||
|
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1340,6 +1340,9 @@ const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
|
||||||
|
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
||||||
|
|
||||||
const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
|
const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
|
||||||
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
||||||
);
|
);
|
||||||
@@ -5664,12 +5667,17 @@ async function main(options = {}) {
|
|||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if we can reuse an existing OpenCode process from a previous HMR cycle
|
|
||||||
syncFromHmrState();
|
syncFromHmrState();
|
||||||
if (await isOpenCodeProcessHealthy()) {
|
if (await isOpenCodeProcessHealthy()) {
|
||||||
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
|
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
|
||||||
|
} else if (ENV_SKIP_OPENCODE_START && ENV_CONFIGURED_OPENCODE_PORT) {
|
||||||
|
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
|
||||||
|
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||||
|
isOpenCodeReady = true;
|
||||||
|
lastOpenCodeError = null;
|
||||||
|
openCodeNotReadySince = 0;
|
||||||
|
syncToHmrState();
|
||||||
} else {
|
} else {
|
||||||
// No healthy process, start fresh
|
|
||||||
if (ENV_CONFIGURED_OPENCODE_PORT) {
|
if (ENV_CONFIGURED_OPENCODE_PORT) {
|
||||||
console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
||||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||||
|
|||||||
Reference in New Issue
Block a user