feat: remove legacy OpenCode runtime
Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
This commit is contained in:
@@ -6,21 +6,6 @@ import { app, BrowserWindow, nativeImage, safeStorage, shell } from 'electron';
|
||||
import type { Server } from 'node:http';
|
||||
import { createServer } from 'node:http';
|
||||
import { join } from 'node:path';
|
||||
import { OpencodeManager } from '../opencode/manager';
|
||||
import { buildOpencodeRuntimeConfigFromNianCodeProviders } from '../opencode/provider-config';
|
||||
import {
|
||||
PLAYWRIGHT_MCP_SERVER_ID,
|
||||
resolvePlaywrightMcpServer,
|
||||
} from '../opencode/playwright-mcp';
|
||||
import { resolveOpencodeRuntimePaths } from '../opencode/paths';
|
||||
import {
|
||||
resolveBundledAgentBrowserPluginPath,
|
||||
resolveBundledCourseSkillsDir,
|
||||
} from '../opencode/course-skills';
|
||||
import {
|
||||
createProjectStore,
|
||||
type OpencodeProjectStore,
|
||||
} from '../opencode/project-store';
|
||||
import { registerIpcHandlers } from './ipc-handlers';
|
||||
import { createTray } from './tray';
|
||||
import { createMenu } from './menu';
|
||||
@@ -29,7 +14,6 @@ import { getNativeWindowMaterialOptions } from './window-material';
|
||||
|
||||
import { appUpdater, registerUpdateHandlers } from './updater';
|
||||
import { logger } from '../utils/logger';
|
||||
import { resolvePythonRuntime, resolveUvRuntime } from '../utils/python-runtime';
|
||||
import { initTelemetry } from '../utils/telemetry';
|
||||
|
||||
import { isQuitting, setQuitting } from './app-state';
|
||||
@@ -86,7 +70,6 @@ import {
|
||||
import { shouldUseSecureWorksSquareSessionPersistence } from '../services/works-square-session-persistence-policy';
|
||||
import { initializeRememberedPassword } from '../services/remembered-password';
|
||||
import { clearManagedWorksSquareRuntimeBestEffort } from '../services/works-square-runtime';
|
||||
import { getPort } from '../utils/config';
|
||||
import { initializeMeowaGameAssetsCredential } from '../api/routes/meowa-game-assets';
|
||||
import {
|
||||
isLocalImageWorkspaceDevelopmentEnabled,
|
||||
@@ -94,7 +77,11 @@ import {
|
||||
} from '../image-workspace/local-workspace';
|
||||
import { WorksSquareDesignWorkspace } from '../image-workspace/works-square-workspace';
|
||||
import type { DesignWorkspaceModule } from '../image-workspace/module';
|
||||
import { createElectronCodingProjectStorage } from '../coding-projects/project-store';
|
||||
import {
|
||||
createCodingProjectStore,
|
||||
createElectronCodingProjectStorage,
|
||||
type CodingProjectStore,
|
||||
} from '../coding-projects/project-store';
|
||||
import { resolveBundledCodingSkillsDir } from '../coding-runtime/pi/resource-loader';
|
||||
import type { CodingProductComposition } from '../api/coding-product-services';
|
||||
import {
|
||||
@@ -112,15 +99,6 @@ const useSecureWorksSquareSessionPersistence = shouldUseSecureWorksSquareSession
|
||||
app.isPackaged,
|
||||
);
|
||||
|
||||
async function buildMakeloreOpencodeRuntimeConfig() {
|
||||
return await buildOpencodeRuntimeConfigFromNianCodeProviders({
|
||||
localProxyApiKey: getHostApiToken() || undefined,
|
||||
mcpServers: {
|
||||
[PLAYWRIGHT_MCP_SERVER_ID]: resolvePlaywrightMcpServer(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isE2EMode && requestedUserDataDir) {
|
||||
app.setPath('userData', requestedUserDataDir);
|
||||
}
|
||||
@@ -197,8 +175,7 @@ const executableSnapshotAtLaunch = snapshotExecutable(process.execPath);
|
||||
|
||||
// Global references
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let opencodeManager!: OpencodeManager;
|
||||
let opencodeProjectStore!: OpencodeProjectStore;
|
||||
let codingProjectStore!: CodingProjectStore;
|
||||
let hostEventBus!: HostEventBus;
|
||||
let hostApiServer: Server | null = null;
|
||||
let projectProgressSync: ReturnType<typeof createProjectProgressSync> | null = null;
|
||||
@@ -390,7 +367,7 @@ function createMainWindow(): BrowserWindow {
|
||||
}
|
||||
if (!session && reason === 'terminal') {
|
||||
void clearManagedWorksSquareRuntimeBestEffort({
|
||||
opencodeManager,
|
||||
codingProducts: codingProducts ?? undefined,
|
||||
imageWorkspace: imageWorkspaceModule ?? undefined,
|
||||
}, 'terminal session invalidation', previousSession?.accessToken);
|
||||
}
|
||||
@@ -500,8 +477,8 @@ async function initialize(): Promise<void> {
|
||||
// construct the first window. Authentication and remote setup are restored
|
||||
// after the renderer has had a chance to paint.
|
||||
const codingProjectStorage = await createElectronCodingProjectStorage();
|
||||
opencodeProjectStore = createProjectStore(codingProjectStorage);
|
||||
worksSubmissionBinding = createWorksSubmissionBindingStore(opencodeProjectStore);
|
||||
codingProjectStore = createCodingProjectStore(codingProjectStorage);
|
||||
worksSubmissionBinding = createWorksSubmissionBindingStore(codingProjectStore);
|
||||
|
||||
const localImageWorkspaceEnabled = isLocalImageWorkspaceDevelopmentEnabled({
|
||||
isPackaged: app.isPackaged,
|
||||
@@ -537,7 +514,7 @@ async function initialize(): Promise<void> {
|
||||
window.webContents.send('lifecycle:sleep');
|
||||
}
|
||||
},
|
||||
onStopRuntime: () => opencodeManager.stop(),
|
||||
onStopRuntime: async () => await codingProducts?.sleep(),
|
||||
});
|
||||
const releaseUtilityPreparer = createReleaseUtilityPreparer({
|
||||
runtimeContext: {
|
||||
@@ -557,28 +534,31 @@ async function initialize(): Promise<void> {
|
||||
window.webContents.send('release-job:status', status);
|
||||
}
|
||||
});
|
||||
const codingAppPath = app.isPackaged
|
||||
? app.getAppPath()
|
||||
: join(__dirname, '..', '..');
|
||||
codingProducts = createCodingComposition({
|
||||
storage: codingProjectStorage,
|
||||
projectStore: codingProjectStore,
|
||||
browser: agentBrowser,
|
||||
localProxyCredential: getHostApiToken() || undefined,
|
||||
paths: {
|
||||
...resolveCodingPiRuntimePaths({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
appPath: codingAppPath,
|
||||
executablePath: process.execPath,
|
||||
}),
|
||||
userDataDir: app.getPath('userData'),
|
||||
bundledSkillsDir: resolveBundledCodingSkillsDir({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
appPath: codingAppPath,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const hostApiContext: HostApiContext = {
|
||||
opencodeManager,
|
||||
opencodeProjectStore,
|
||||
codingProjectStore,
|
||||
eventBus: hostEventBus,
|
||||
mainWindow: window,
|
||||
agentBrowser,
|
||||
@@ -588,7 +568,7 @@ async function initialize(): Promise<void> {
|
||||
releaseJobs,
|
||||
codingProducts,
|
||||
};
|
||||
registerIpcHandlers(undefined, opencodeManager, undefined, window, backgroundLifecycle, hostApiContext);
|
||||
registerIpcHandlers(window, backgroundLifecycle, hostApiContext);
|
||||
|
||||
void initializeRememberedPassword({
|
||||
secureStorage: useSecureWorksSquareSessionPersistence ? safeStorage : null,
|
||||
@@ -635,13 +615,13 @@ async function initialize(): Promise<void> {
|
||||
if (consumeWorksSquareStartupRuntimeCleanupRequired()) {
|
||||
await runDeferred('expired runtime cleanup', async () => {
|
||||
await clearManagedWorksSquareRuntimeBestEffort({
|
||||
opencodeManager,
|
||||
codingProducts: codingProducts ?? undefined,
|
||||
imageWorkspace,
|
||||
}, 'expired persisted session during startup');
|
||||
});
|
||||
}
|
||||
if (!isE2EMode) {
|
||||
projectProgressSync = createProjectProgressSync(opencodeProjectStore);
|
||||
projectProgressSync = createProjectProgressSync(codingProjectStore);
|
||||
const activateProgrammingServices = (): void => {
|
||||
void projectProgressSync?.start()
|
||||
.then(() => projectProgressSync?.activate())
|
||||
@@ -665,21 +645,6 @@ async function initialize(): Promise<void> {
|
||||
}
|
||||
};
|
||||
|
||||
opencodeManager.on('status', (status) => {
|
||||
logger.info(
|
||||
`opencode runtime status: state=${status.state}, port=${status.port}, pid=${status.pid ?? 'n/a'}, url=${status.url ?? 'n/a'}${status.error ? `, error=${status.error}` : ''}`,
|
||||
);
|
||||
hostEventBus.emit('opencode:status', status);
|
||||
});
|
||||
|
||||
opencodeManager.on('stderr', (message) => {
|
||||
const trimmed = message.trim();
|
||||
if (trimmed) {
|
||||
logger.warn(`[opencode stderr] ${trimmed}`);
|
||||
}
|
||||
hostEventBus.emit('opencode:stderr', { message });
|
||||
});
|
||||
|
||||
browserOAuthManager.on('oauth:start', (payload) => {
|
||||
hostEventBus.emit('oauth:start', payload);
|
||||
});
|
||||
@@ -696,9 +661,8 @@ async function initialize(): Promise<void> {
|
||||
hostEventBus.emit('oauth:error', error);
|
||||
});
|
||||
|
||||
// OpenCode is started by the Code module on demand. Keeping the runtime
|
||||
// cold for Canvas/Learning/Robot sessions avoids a resident child process
|
||||
// and its project event stream on every launch.
|
||||
// Coding workers are started on demand. Keeping them cold for
|
||||
// Canvas/Learning/Robot avoids a resident child process on every launch.
|
||||
void initializeBackgroundServices().catch((error) => {
|
||||
logger.warn('Deferred background initialization failed:', error);
|
||||
});
|
||||
@@ -725,60 +689,6 @@ if (gotTheLock) {
|
||||
app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID);
|
||||
}
|
||||
|
||||
const opencodePaths = resolveOpencodeRuntimePaths({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
});
|
||||
const pythonRuntime = resolvePythonRuntime({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
configuredPath: process.env.NIANCODE_PYTHON_PATH,
|
||||
});
|
||||
const uvRuntime = resolveUvRuntime({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
});
|
||||
const bundledCourseSkillsDir = resolveBundledCourseSkillsDir({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
});
|
||||
const bundledAgentBrowserPluginPath = resolveBundledAgentBrowserPluginPath({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
});
|
||||
opencodeManager = new OpencodeManager({
|
||||
port: getPort('OPENCODE_RUNTIME'),
|
||||
binPath: opencodePaths.binPath,
|
||||
preflightPreferredPort: true,
|
||||
userDataDir: app.getPath('userData'),
|
||||
bundledCourseSkillsDir,
|
||||
bundledAgentBrowserPluginPath,
|
||||
pythonRuntime,
|
||||
uvRuntime,
|
||||
runtimeConfigProvider: async () => {
|
||||
const runtime = await buildMakeloreOpencodeRuntimeConfig();
|
||||
return {
|
||||
...runtime,
|
||||
config: runtime.config as unknown as Record<string, unknown>,
|
||||
env: {
|
||||
...runtime.env,
|
||||
NIANCODE_HOST_API_BASE_URL: `http://127.0.0.1:${getPort('NIANCODE_HOST_API')}`,
|
||||
NIANCODE_HOST_API_TOKEN: getHostApiToken(),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
hostEventBus = new HostEventBus();
|
||||
|
||||
// When a second instance is launched, focus the existing window instead.
|
||||
@@ -857,9 +767,6 @@ if (gotTheLock) {
|
||||
backgroundLifecycle?.dispose();
|
||||
releaseJobs?.dispose();
|
||||
|
||||
const stopOpencodePromise = opencodeManager.stop().catch((err) => {
|
||||
logger.warn('opencodeManager.stop() error during quit:', err);
|
||||
});
|
||||
const stopAgentBrowserPromise = agentBrowser?.dispose().catch((err) => {
|
||||
logger.warn('agentBrowser.dispose() error during quit:', err);
|
||||
}) ?? Promise.resolve();
|
||||
@@ -870,7 +777,6 @@ if (gotTheLock) {
|
||||
logger.warn('codingProducts.shutdown() error during quit:', err);
|
||||
}) ?? Promise.resolve();
|
||||
const stopPromise = Promise.allSettled([
|
||||
stopOpencodePromise,
|
||||
stopAgentBrowserPromise,
|
||||
closeImageWorkspacePromise,
|
||||
stopCodingProductsPromise,
|
||||
@@ -881,7 +787,7 @@ if (gotTheLock) {
|
||||
|
||||
void Promise.race([stopPromise.then(() => 'stopped' as const), timeoutPromise]).then((result) => {
|
||||
if (result === 'timeout') {
|
||||
logger.warn('opencode shutdown timed out during app quit; proceeding with forced quit');
|
||||
logger.warn('runtime shutdown timed out during app quit; proceeding with forced quit');
|
||||
}
|
||||
markQuitCleanupCompleted(quitLifecycleState);
|
||||
app.quit();
|
||||
@@ -889,8 +795,6 @@ if (gotTheLock) {
|
||||
});
|
||||
|
||||
// Best-effort runtime cleanup on unexpected crashes.
|
||||
// These handlers attempt to terminate the opencode child process within a
|
||||
// short timeout before force-exiting, preventing orphaned processes.
|
||||
const emergencyRuntimeCleanup = (reason: string, error: unknown): void => {
|
||||
logger.error(`${reason}:`, error);
|
||||
projectProgressSync?.stop();
|
||||
@@ -899,11 +803,6 @@ if (gotTheLock) {
|
||||
} catch {
|
||||
// ignore — dispose() may not be callable if state is corrupted
|
||||
}
|
||||
try {
|
||||
void opencodeManager?.stop().catch(() => { /* ignore */ });
|
||||
} catch {
|
||||
// ignore — stop() may not be callable if state is corrupted
|
||||
}
|
||||
try {
|
||||
void codingProducts?.shutdown().catch(() => { /* ignore */ });
|
||||
} catch {
|
||||
@@ -925,7 +824,7 @@ if (gotTheLock) {
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
export { mainWindow, opencodeManager, opencodeProjectStore };
|
||||
export { mainWindow, codingProjectStore };
|
||||
|
||||
export async function runLocalPreviewPreflightE2E(
|
||||
scenario: 'success' | 'external' = 'success',
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* IPC Handlers
|
||||
* Registers the renderer channels used by the opencode-first desktop app.
|
||||
* Registers the renderer channels used by the desktop app.
|
||||
*/
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
||||
import type { OpencodeManager } from '../opencode/manager';
|
||||
import { registerHostApiProxyHandlers } from './ipc/host-api-proxy';
|
||||
import { registerTranscriptExportHandler } from './ipc/transcript-export';
|
||||
import { applyProxySettings } from './proxy';
|
||||
@@ -184,7 +183,6 @@ async function handleUnifiedProvider(action: string, payload: unknown): Promise<
|
||||
|
||||
async function handleUnifiedRequest(
|
||||
request: UnifiedRequest,
|
||||
opencodeManager: OpencodeManager,
|
||||
): Promise<UnifiedResponse> {
|
||||
const id = request.id;
|
||||
const module = request.module || '';
|
||||
@@ -198,11 +196,6 @@ async function handleUnifiedRequest(
|
||||
return unsupported(id, module, action);
|
||||
}
|
||||
|
||||
if (module === 'opencode') {
|
||||
if (action === 'status') return ok(id, opencodeManager.getStatus());
|
||||
return unsupported(id, module, action);
|
||||
}
|
||||
|
||||
if (module === 'settings') {
|
||||
if (action === 'getAll') return ok(id, await getAllSettings());
|
||||
if (action === 'get') {
|
||||
@@ -241,9 +234,6 @@ async function handleUnifiedRequest(
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(
|
||||
_legacyRuntimeManager: unknown,
|
||||
opencodeManager: OpencodeManager,
|
||||
_legacyMarketplaceService: unknown,
|
||||
mainWindow: BrowserWindow,
|
||||
lifecycle?: BackgroundLifecycleController,
|
||||
hostApiContext?: HostApiContext,
|
||||
@@ -262,7 +252,7 @@ export function registerIpcHandlers(
|
||||
rendererWebContents.on('destroyed', releaseRendererLeases);
|
||||
}
|
||||
ipcMain.handle('app:request', async (_event, request: UnifiedRequest) => (
|
||||
handleUnifiedRequest(request, opencodeManager)
|
||||
handleUnifiedRequest(request)
|
||||
));
|
||||
|
||||
ipcMain.handle('app:version', () => app.getVersion());
|
||||
@@ -276,15 +266,6 @@ export function registerIpcHandlers(
|
||||
});
|
||||
ipcMain.handle('app:performance', () => collectPerformanceSnapshot(lifecycle));
|
||||
|
||||
ipcMain.handle('opencode:status', () => opencodeManager.getStatus());
|
||||
ipcMain.handle('opencode:start', () => opencodeManager.start());
|
||||
ipcMain.handle('opencode:stop', async () => {
|
||||
await opencodeManager.stop();
|
||||
return opencodeManager.getStatus();
|
||||
});
|
||||
ipcMain.handle('opencode:restart', () => opencodeManager.restart());
|
||||
ipcMain.handle('opencode:health', () => opencodeManager.checkHealth());
|
||||
|
||||
ipcMain.handle('lifecycle:activity', (_event, activity: DesktopActivity) => {
|
||||
const nextActivity = activity && typeof activity === 'object'
|
||||
? activity
|
||||
|
||||
Reference in New Issue
Block a user