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:
2026-08-24 12:17:43 +08:00
parent 61fede2b4d
commit 5a275b93a7
199 changed files with 1330 additions and 64725 deletions

View File

@@ -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',

View File

@@ -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