import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import { readFileSync, rmSync } from 'fs' let visualizationRequestId = 0 async function invokeVisualizationStart(channel: string, args: unknown[], onProgress?: (message: string) => void) { const requestId = `${Date.now()}:${++visualizationRequestId}` const listener = (_event: Electron.IpcRendererEvent, id: string, message: string) => { if (id === requestId) onProgress?.(message) } ipcRenderer.on('visualization:progress', listener) try { return await ipcRenderer.invoke(channel, ...args, requestId) } finally { ipcRenderer.removeListener('visualization:progress', listener) } } type LauncherAuthPayload = { appUrl?: string | null handoffToken?: string | null audienceApp?: string | null } type RemoteWorkspaceTarget = { host: string user?: string | null port?: number | null workspaceRoot?: string | null } type ModulePrebuiltArtifact = { type?: string | null url?: string | null fileName?: string | null checksumSha256?: string | null extractPath?: string | null architecture?: string | null required?: boolean | null } type RuntimeImageArtifact = { image?: string | null url?: string | null fileName?: string | null checksumSha256?: string | null required?: boolean | null } type ModuleInstallOptions = { prebuiltArtifacts?: ModulePrebuiltArtifact[] | null runtimeImages?: RuntimeImageArtifact[] | null } type RosNetworkConfig = { enabled?: boolean | null allowExternal?: boolean peer?: RemoteWorkspaceTarget | null } type SavedMapArtifactName = 'yaml' | 'pgm' | 'database' type SavedMapArtifactPath = { name: SavedMapArtifactName path: string } type SavedMapArtifactDownload = { name: SavedMapArtifactName fileName: string url: string } const decodeArgValue = (prefix: string): string | null => { const rawValue = process.argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length) if (!rawValue) return null try { return decodeURIComponent(rawValue) } catch { return rawValue } } const readLauncherAuthFile = (): LauncherAuthPayload | null => { const filePath = process.env.ATNMY_HANDOFF_FILE if (!filePath) { console.info('[lms-preload] launcher auth file env is empty') return null } try { const payload = JSON.parse(readFileSync(filePath, 'utf8')) as LauncherAuthPayload rmSync(filePath, { force: true }) console.info('[lms-preload] launcher auth file loaded', { hasHandoffToken: Boolean(payload.handoffToken), audienceApp: payload.audienceApp ?? null }) return payload } catch (error) { console.warn('[lms-preload] launcher auth file read failed', error) return null } } const launcherAuthFile = readLauncherAuthFile() const launcherAuth = { appUrl: launcherAuthFile?.appUrl ?? decodeArgValue('--url=') ?? process.env.ATNMY_APP_URL ?? null, handoffToken: launcherAuthFile?.handoffToken ?? decodeArgValue('--handoff-token=') ?? process.env.ATNMY_HANDOFF_TOKEN ?? null, audienceApp: launcherAuthFile?.audienceApp ?? decodeArgValue('--audience-app=') ?? process.env.ATNMY_AUDIENCE_APP ?? 'LMS' } console.info('[lms-preload] launcher auth resolved', { hasHandoffToken: Boolean(launcherAuth.handoffToken), audienceApp: launcherAuth.audienceApp }) contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('launcherAuth', launcherAuth) contextBridge.exposeInMainWorld('lmsBootSplash', { status: (message: string) => ipcRenderer.send('lms-boot-status', message), done: () => ipcRenderer.send('lms-boot-done') }) contextBridge.exposeInMainWorld('titlebar', { minimize: () => ipcRenderer.send('titlebar-minimize'), maximize: () => ipcRenderer.send('titlebar-maximize'), openDevTools: () => ipcRenderer.invoke('titlebar-open-devtools'), close: () => ipcRenderer.send('titlebar-close') }) contextBridge.exposeInMainWorld('appDiagnostics', { read: () => ipcRenderer.invoke('app-diagnostics:read') }) contextBridge.exposeInMainWorld('appLifecycle', { quit: () => ipcRenderer.send('app-quit') }) contextBridge.exposeInMainWorld('learningVideo', { open: (url: string, title?: string) => ipcRenderer.invoke('learning-video:open', { url, title }) }) contextBridge.exposeInMainWorld('moduleFiles', { read: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:read', moduleName, workspaceRoot, folderName), image: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:image', moduleName, path, workspaceRoot, folderName), create: (moduleName: string, path: string, kind: 'file' | 'folder', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:create', moduleName, path, kind, workspaceRoot, folderName), write: (moduleName: string, path: string, content: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:write', moduleName, path, content, workspaceRoot, folderName), copy: (moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:copy', moduleName, fromPath, toPath, mode, workspaceRoot, folderName), rename: (moduleName: string, fromPath: string, toPath: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:rename', moduleName, fromPath, toPath, workspaceRoot, folderName), delete: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:delete', moduleName, path, workspaceRoot, folderName) }) contextBridge.exposeInMainWorld('modules', { list: (workspaceRoot?: string, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('modules:list', workspaceRoot, modules), install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options), delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:delete', moduleName, workspaceRoot, folderName), start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => ipcRenderer.invoke('modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams), stop: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:stop', moduleName, workspaceRoot, folderName), stopMany: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('modules:stop-many', modules, workspaceRoot), stopRunning: () => ipcRenderer.invoke('modules:stop-running'), restart: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('modules:restart', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), logs: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:logs', moduleName, workspaceRoot, folderName), updateParameters: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string) => ipcRenderer.invoke('modules:update-parameters', moduleName, workspaceRoot, folderName, rosParams) }) contextBridge.exposeInMainWorld('git', { status: (moduleName: string, workspaceRoot?: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('git:status', moduleName, workspaceRoot, folderName, selectedFilePath), submit: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string, repositoryUrl?: string) => ipcRenderer.invoke('git:submit', moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId, repositoryUrl), teamPublish: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string) => ipcRenderer.invoke('git:team-publish', moduleName, workspaceRoot, folderName, message, authorName, authorEmail), teamPull: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('git:team-pull', moduleName, workspaceRoot, folderName), submissions: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('git:submissions', modules, workspaceRoot), commitDiff: (moduleName: string, workspaceRoot?: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('git:commit-diff', moduleName, workspaceRoot, folderName, commitHash) }) contextBridge.exposeInMainWorld('exampleCode', { read: () => ipcRenderer.invoke('example-code:read') }) contextBridge.exposeInMainWorld('terminal', { start: (moduleName?: string, cols?: number, rows?: number, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('terminal-start', moduleName, cols, rows, sessionId, workspaceRoot, folderName), resize: (cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('terminal-resize', cols, rows, sessionId), changeDirectory: (moduleName: string, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('terminal-change-directory', moduleName, sessionId, workspaceRoot, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('terminal-write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('terminal-stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } }) contextBridge.exposeInMainWorld('visualization', { status: () => ipcRenderer.invoke('visualization:status'), start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('visualization:start', stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork), stop: (environmentProfile?: unknown) => ipcRenderer.invoke('visualization:stop', environmentProfile) }) contextBridge.exposeInMainWorld('localWorkspace', { docker: { status: () => ipcRenderer.invoke('local-workspace:docker:status'), openGuide: () => ipcRenderer.invoke('local-workspace:docker:guide'), }, moduleFiles: { read: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:read', moduleName, workspaceRoot, folderName), image: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:image', moduleName, path, workspaceRoot, folderName), create: (moduleName: string, path: string, kind: 'file' | 'folder', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:create', moduleName, path, kind, workspaceRoot, folderName), write: (moduleName: string, path: string, content: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:write', moduleName, path, content, workspaceRoot, folderName), copy: (moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:copy', moduleName, fromPath, toPath, mode, workspaceRoot, folderName), rename: (moduleName: string, fromPath: string, toPath: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:rename', moduleName, fromPath, toPath, workspaceRoot, folderName), delete: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:delete', moduleName, path, workspaceRoot, folderName) }, modules: { list: (workspaceRoot?: string, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('local-workspace:modules:list', workspaceRoot, modules), listDockerImages: () => ipcRenderer.invoke('local-workspace:modules:docker-images'), install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('local-workspace:modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options), createCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => ipcRenderer.invoke('local-workspace:modules:create-custom', workspaceRoot, input), selectImportSource: () => ipcRenderer.invoke('local-workspace:modules:select-import-source'), importCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string; sourcePath: string }) => ipcRenderer.invoke('local-workspace:modules:import-custom', workspaceRoot, input), delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:delete', moduleName, workspaceRoot, folderName), start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('local-workspace:modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => ipcRenderer.invoke('local-workspace:modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams), stop: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:stop', moduleName, workspaceRoot, folderName), stopMany: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('local-workspace:modules:stop-many', modules, workspaceRoot), stopRunning: () => ipcRenderer.invoke('local-workspace:modules:stop-running'), restart: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('local-workspace:modules:restart', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), logs: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:logs', moduleName, workspaceRoot, folderName), updateParameters: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string) => ipcRenderer.invoke('local-workspace:modules:update-parameters', moduleName, workspaceRoot, folderName, rosParams) }, rosGraph: { snapshot: () => ipcRenderer.invoke('local-workspace:ros-graph') }, nav2: { setSimulatorPose: (pose: { x: number; y: number; yaw: number }) => ipcRenderer.invoke('local-workspace:nav2:set-simulator-pose', pose), addRuntimeObstacle: (obstacle: { id: string; x: number; y: number; yaw: number; length: number; depth: number; height: number }) => ipcRenderer.invoke('local-workspace:nav2:add-runtime-obstacle', obstacle), clearRuntimeObstacles: () => ipcRenderer.invoke('local-workspace:nav2:clear-runtime-obstacles') }, worldLayout: { read: (workspaceRoot?: string) => ipcRenderer.invoke('local-workspace:world-layout:read', workspaceRoot), write: (workspaceRoot: string | undefined, layout: unknown) => ipcRenderer.invoke('local-workspace:world-layout:write', workspaceRoot, layout) }, maps: { readArtifacts: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactPath[]) => ipcRenderer.invoke('local-workspace:maps:read-artifacts', workspaceRoot, artifacts), sync: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactDownload[]) => ipcRenderer.invoke('local-workspace:maps:sync', workspaceRoot, artifacts), generateCollisionLayout: (workspaceRoot: string | undefined, yamlPath: string, pgmPath: string) => ipcRenderer.invoke('local-workspace:maps:generate-collision-layout', workspaceRoot, yamlPath, pgmPath), generatePointCloudCollisionLayout: (workspaceRoot: string | undefined, databasePath: string) => ipcRenderer.invoke('local-workspace:maps:generate-point-cloud-collision-layout', workspaceRoot, databasePath) }, visualization: { status: () => ipcRenderer.invoke('local-workspace:visualization:status'), start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null, onProgress?: (message: string) => void) => invokeVisualizationStart('local-workspace:visualization:start', [stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath], onProgress), stop: (environmentProfile?: unknown) => ipcRenderer.invoke('local-workspace:visualization:stop', environmentProfile) }, git: { status: (moduleName: string, workspaceRoot?: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('local-workspace:git:status', moduleName, workspaceRoot, folderName, selectedFilePath), submit: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string, repositoryUrl?: string) => ipcRenderer.invoke('local-workspace:git:submit', moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId, repositoryUrl), teamPublish: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string) => ipcRenderer.invoke('local-workspace:git:team-publish', moduleName, workspaceRoot, folderName, message, authorName, authorEmail), teamPull: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:git:team-pull', moduleName, workspaceRoot, folderName), restoreFile: (moduleName: string, workspaceRoot?: string, folderName?: string, filePath?: string, status?: string) => ipcRenderer.invoke('local-workspace:git:restore-file', moduleName, workspaceRoot, folderName, filePath, status), submissions: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('local-workspace:git:submissions', modules, workspaceRoot), commitDiff: (moduleName: string, workspaceRoot?: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('local-workspace:git:commit-diff', moduleName, workspaceRoot, folderName, commitHash) }, terminal: { start: (moduleName?: string, cols?: number, rows?: number, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:terminal:start', moduleName, cols, rows, sessionId, workspaceRoot, folderName), resize: (cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('local-workspace:terminal:resize', cols, rows, sessionId), changeDirectory: (moduleName: string, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:terminal:change-directory', moduleName, sessionId, workspaceRoot, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('local-workspace:terminal:write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('local-workspace:terminal:stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } } }) contextBridge.exposeInMainWorld('remoteWorkspace', { moduleFiles: { read: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:read', target, moduleName, folderName), image: (target: RemoteWorkspaceTarget, moduleName: string, path: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:image', target, moduleName, path, folderName), create: (target: RemoteWorkspaceTarget, moduleName: string, path: string, kind: 'file' | 'folder', folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:create', target, moduleName, path, kind, folderName), write: (target: RemoteWorkspaceTarget, moduleName: string, path: string, content: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:write', target, moduleName, path, content, folderName), copy: (target: RemoteWorkspaceTarget, moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:copy', target, moduleName, fromPath, toPath, mode, folderName), rename: (target: RemoteWorkspaceTarget, moduleName: string, fromPath: string, toPath: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:rename', target, moduleName, fromPath, toPath, folderName), delete: (target: RemoteWorkspaceTarget, moduleName: string, path: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:delete', target, moduleName, path, folderName) }, maps: { readArtifacts: (target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactPath[]) => ipcRenderer.invoke('remote-workspace:maps:read-artifacts', target, artifacts), copyActiveRtabmapDatabaseToLocal: (target: RemoteWorkspaceTarget, localWorkspaceRoot: string | undefined, destinationPath: string) => ipcRenderer.invoke('remote-workspace:maps:copy-active-rtabmap-database', target, localWorkspaceRoot, destinationPath), uploadRtabmapDatabase: (target: RemoteWorkspaceTarget, uploadUrl: string, databasePath?: string | null) => ipcRenderer.invoke('remote-workspace:maps:upload-rtabmap-database', target, uploadUrl, databasePath), sync: (target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactDownload[]) => ipcRenderer.invoke('remote-workspace:maps:sync', target, artifacts) }, modules: { list: (target: RemoteWorkspaceTarget, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:list', target, modules), install: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('remote-workspace:modules:install', target, moduleName, folderName, templateRepositoryUrl, studentRepositoryUrl, options), createCustom: (target: RemoteWorkspaceTarget, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => ipcRenderer.invoke('remote-workspace:modules:create-custom', target, input), transfer: (target: RemoteWorkspaceTarget, sourceWorkspaceRoot: string | undefined, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:modules:transfer', target, sourceWorkspaceRoot, moduleName, folderName), start: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('remote-workspace:modules:start', target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), build: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('remote-workspace:modules:build', target, moduleName, folderName, gpuEnabled, rosDomainId, runtimeImages), stop: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:modules:stop', target, moduleName, folderName), stopMany: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:stop-many', target, modules), resetSession: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:reset-session', target, modules), restart: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('remote-workspace:modules:restart', target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork), logs: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:modules:logs', target, moduleName, folderName) }, rosGraph: { snapshot: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:ros-graph', target) }, ros: { resetSession: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:ros:reset-session', target) }, visualization: { status: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:status', target), start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, onProgress?: (message: string) => void) => invokeVisualizationStart('remote-workspace:visualization:start', [target, stack, rosDomainId, environmentProfile, rosNetwork], onProgress), stop: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:stop', target) }, git: { status: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('remote-workspace:git:status', target, moduleName, folderName, selectedFilePath), submit: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string, repositoryUrl?: string) => ipcRenderer.invoke('remote-workspace:git:submit', target, moduleName, folderName, message, authorName, authorEmail, submissionId, repositoryUrl), teamPublish: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string) => ipcRenderer.invoke('remote-workspace:git:team-publish', target, moduleName, folderName, message, authorName, authorEmail), teamPull: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:git:team-pull', target, moduleName, folderName), restoreFile: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, filePath?: string, status?: string) => ipcRenderer.invoke('remote-workspace:git:restore-file', target, moduleName, folderName, filePath, status), submissions: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => ipcRenderer.invoke('remote-workspace:git:submissions', target, modules), commitDiff: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('remote-workspace:git:commit-diff', target, moduleName, folderName, commitHash) }, terminal: { start: (target: RemoteWorkspaceTarget, moduleName?: string, cols?: number, rows?: number, sessionId?: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:terminal:start', target, moduleName, cols, rows, sessionId, folderName), resize: (target: RemoteWorkspaceTarget, cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('remote-workspace:terminal:resize', target, cols, rows, sessionId), changeDirectory: (target: RemoteWorkspaceTarget, moduleName: string, sessionId?: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:terminal:change-directory', target, moduleName, sessionId, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('remote-workspace:terminal:write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('remote-workspace:terminal:stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, incomingSessionId: string, data: string) => { if (incomingSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, incomingSessionId: string, data: string) => { if (incomingSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } } })