Transform the session picker from a modal into a comprehensive welcome screen that displays instance metadata, session history, and creation options. The new view provides full keyboard navigation, shows MCP server status, OpenCode binary path, and project information in a compact, space-efficient layout. Key features: - Instance metadata sidebar showing folder, project, VCS, version, binary path, MCP status, and server info - Session list with keyboard navigation (↑↓, PgUp/PgDn, Home/End, Enter) - Cmd+Enter shortcut to create new session from anywhere - Compact design with efficient space usage - Visual MCP server status indicators (running/stopped/error) - Scrollable layout that keeps "New Session" section accessible
132 lines
3.4 KiB
TypeScript
132 lines
3.4 KiB
TypeScript
import { ipcMain, BrowserWindow } from "electron"
|
|
import { processManager } from "./process-manager"
|
|
import { randomBytes } from "crypto"
|
|
import * as fs from "fs"
|
|
import * as path from "path"
|
|
import ignore from "ignore"
|
|
|
|
interface Instance {
|
|
id: string
|
|
folder: string
|
|
port: number
|
|
pid: number
|
|
status: "starting" | "ready" | "error" | "stopped"
|
|
error?: string
|
|
}
|
|
|
|
const instances = new Map<string, Instance>()
|
|
|
|
function generateId(): string {
|
|
return randomBytes(16).toString("hex")
|
|
}
|
|
|
|
export function setupInstanceIPC(mainWindow: BrowserWindow) {
|
|
processManager.setMainWindow(mainWindow)
|
|
|
|
ipcMain.handle("instance:create", async (event, id: string, folder: string) => {
|
|
const instance: Instance = {
|
|
id,
|
|
folder,
|
|
port: 0,
|
|
pid: 0,
|
|
status: "starting",
|
|
}
|
|
|
|
instances.set(id, instance)
|
|
|
|
try {
|
|
const { pid, port, binaryPath } = await processManager.spawn(folder, id)
|
|
|
|
instance.port = port
|
|
instance.pid = pid
|
|
instance.status = "ready"
|
|
|
|
mainWindow.webContents.send("instance:started", { id, port, pid, binaryPath })
|
|
|
|
const meta = processManager.getAllProcesses().get(pid)
|
|
if (meta) {
|
|
meta.childProcess.on("exit", (code, signal) => {
|
|
instance.status = "stopped"
|
|
mainWindow.webContents.send("instance:stopped", { id })
|
|
})
|
|
}
|
|
|
|
return { id, port, pid, binaryPath }
|
|
} catch (error) {
|
|
instance.status = "error"
|
|
instance.error = error instanceof Error ? error.message : String(error)
|
|
|
|
mainWindow.webContents.send("instance:error", {
|
|
id,
|
|
error: instance.error,
|
|
})
|
|
|
|
throw error
|
|
}
|
|
})
|
|
|
|
ipcMain.handle("instance:stop", async (event, pid: number) => {
|
|
await processManager.kill(pid)
|
|
|
|
for (const [id, instance] of instances.entries()) {
|
|
if (instance.pid === pid) {
|
|
instance.status = "stopped"
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle("instance:status", async (event, pid: number) => {
|
|
return processManager.getStatus(pid)
|
|
})
|
|
|
|
ipcMain.handle("instance:list", async () => {
|
|
return Array.from(instances.values())
|
|
})
|
|
|
|
ipcMain.handle("fs:scanDirectory", async (event, workspaceFolder: string) => {
|
|
const ig = ignore()
|
|
ig.add([".git", "node_modules"])
|
|
|
|
const gitignorePath = path.join(workspaceFolder, ".gitignore")
|
|
if (fs.existsSync(gitignorePath)) {
|
|
const content = fs.readFileSync(gitignorePath, "utf-8")
|
|
ig.add(content)
|
|
}
|
|
|
|
function scanDir(dirPath: string, baseDir: string): string[] {
|
|
const results: string[] = []
|
|
|
|
try {
|
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dirPath, entry.name)
|
|
const relativePath = path.relative(baseDir, fullPath)
|
|
|
|
if (ig.ignores(relativePath)) {
|
|
continue
|
|
}
|
|
|
|
if (entry.isDirectory()) {
|
|
const dirWithSlash = relativePath + "/"
|
|
if (!ig.ignores(dirWithSlash)) {
|
|
results.push(dirWithSlash)
|
|
const subFiles = scanDir(fullPath, baseDir)
|
|
results.push(...subFiles)
|
|
}
|
|
} else {
|
|
results.push(relativePath)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Error scanning ${dirPath}:`, error)
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
return scanDir(workspaceFolder, workspaceFolder)
|
|
})
|
|
}
|