Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions electron/main/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const envPath = envPaths(app.getName(), {suffix: ""})
export const cacheDir = envPath.cache
export const homeDir = os.homedir()
export const appDir = path.join(homeDir, ".dive")
export const binDir = path.join(appDir, "bin")
export const scriptsDir = path.join(appDir, "scripts")
export const configDir = app.isPackaged ? path.join(appDir, "config") : path.join(process.cwd(), ".config")
export const hostCacheDir = path.join(appDir, "host_cache")
Expand Down
2 changes: 2 additions & 0 deletions electron/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ipcLlmHandler } from "./llm"
import { ipcMenuHandler } from "./menu"
import { ipcOapHandler } from "./oap"
import { ipcLocalIPCHandler } from "./lipc"
import { ipcPathHandler } from "./path"

export function ipcHandler(win: BrowserWindow) {
ipcEnvHandler(win)
Expand All @@ -15,4 +16,5 @@ export function ipcHandler(win: BrowserWindow) {
ipcMenuHandler(win)
ipcOapHandler(win)
ipcLocalIPCHandler(win)
ipcPathHandler(win)
}
164 changes: 164 additions & 0 deletions electron/main/ipc/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ipcMain, BrowserWindow } from "electron"
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
import fuzzysort from "fuzzysort"

interface PathEntry {
name: string
path: string
isDir: boolean
}

interface PathSearchResult {
entries: PathEntry[]
error?: string
}

export function ipcPathHandler(_win: BrowserWindow) {
ipcMain.handle("path:search", async (_event, searchPath: string): Promise<PathSearchResult> => {
try {
// Expand ~ to home directory
let normalizedPath = searchPath
if (normalizedPath.startsWith("~")) {
normalizedPath = path.join(os.homedir(), normalizedPath.slice(1))
}

// Determine parent directory and prefix for filtering
let dirToSearch: string
let filterPrefix: string

if (normalizedPath.endsWith(path.sep) || normalizedPath.endsWith("/")) {
// User typed a complete directory path, list its contents
dirToSearch = normalizedPath
filterPrefix = ""
} else {
// User is typing a partial name, list parent and filter
dirToSearch = path.dirname(normalizedPath)
filterPrefix = path.basename(normalizedPath).toLowerCase()
}

// Check if directory exists
if (!fs.existsSync(dirToSearch)) {
return { entries: [], error: "Directory not found" }
}

const stat = fs.statSync(dirToSearch)
if (!stat.isDirectory()) {
return { entries: [], error: "Not a directory" }
}

// Read directory contents
const items = fs.readdirSync(dirToSearch, { withFileTypes: true })

// Filter and map entries
const entries: PathEntry[] = items
.filter(item => {
// Hide dotfiles unless user is searching for them (filter starts with .)
if (item.name.startsWith('.') && !filterPrefix.startsWith('.')) {
return false
}

// Filter by prefix if provided
if (filterPrefix) {
return item.name.toLowerCase().startsWith(filterPrefix)
}
return true
})
.map(item => ({
name: item.name,
path: path.join(dirToSearch, item.name),
isDir: item.isDirectory()
}))
// Sort: directories first, then by name
.sort((a, b) => {
if (a.isDir !== b.isDir) {
return a.isDir ? -1 : 1
}
return a.name.localeCompare(b.name)
})
// Limit to 20 entries
.slice(0, 20)

return { entries }
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error"
return { entries: [], error: message }
}
})

ipcMain.handle("path:fuzzy-search", async (_event, basePath: string, query: string): Promise<PathSearchResult> => {
try {
// Expand ~ to home directory
let normalizedPath = basePath
if (normalizedPath.startsWith("~")) {
normalizedPath = path.join(os.homedir(), normalizedPath.slice(1))
}

// Check if directory exists
if (!fs.existsSync(normalizedPath)) {
return { entries: [], error: "Directory not found" }
}

const stat = fs.statSync(normalizedPath)
if (!stat.isDirectory()) {
return { entries: [], error: "Not a directory" }
}

// Recursively collect all files (with depth limit)
const allPaths: { relativePath: string; fullPath: string; isDir: boolean }[] = []
const maxDepth = 5

function walkDir(dir: string, depth: number, baseDir: string) {
if (depth > maxDepth) return

try {
const items = fs.readdirSync(dir, { withFileTypes: true })
for (const item of items) {
// Skip hidden files unless query starts with .
if (item.name.startsWith('.') && !query.startsWith('.')) {
continue
}

const fullPath = path.join(dir, item.name)
const relativePath = path.relative(baseDir, fullPath)

allPaths.push({
relativePath,
fullPath,
isDir: item.isDirectory()
})

// Recurse into directories
if (item.isDirectory()) {
walkDir(fullPath, depth + 1, baseDir)
}
}
} catch {
// Ignore permission errors
}
}

walkDir(normalizedPath, 0, normalizedPath)

// Fuzzy search using fuzzysort
const results = fuzzysort.go(query, allPaths, {
key: "relativePath",
limit: 20,
threshold: -10000
})

// Map results to PathEntry
const entries: PathEntry[] = results.map(result => ({
name: result.obj.relativePath,
path: result.obj.fullPath,
isDir: result.obj.isDir
}))

return { entries }
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error"
return { entries: [], error: message }
}
})
}
118 changes: 118 additions & 0 deletions electron/main/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import packageJson from "../../package.json"
import { app, BrowserWindow } from "electron"
import path from "node:path"
import fse, { mkdirp } from "fs-extra"
import { pipeline } from "node:stream/promises"
import {
configDir,
DEF_MCP_SERVER_CONFIG,
Expand All @@ -15,6 +16,7 @@ import {
DEF_PLUGIN_CONFIG,
DEF_MCP_SERVER_NAME,
getDefMcpBinPath,
binDir,
} from "./constant.js"
import spawn from "cross-spawn"
import { ChildProcess, SpawnOptions, StdioOptions } from "node:child_process"
Expand All @@ -25,6 +27,12 @@ import { hostCache } from "./store.js"

const baseConfigDir = app.isPackaged ? configDir : path.join(__dirname, "..", "..", ".config")

// Node.js version for Linux
const NODEJS_VERSION = "22.22.0"
const NODEJS_FILE_NAME = `node-v${NODEJS_VERSION}-linux-x64`
const NODEJS_FILE = `${NODEJS_FILE_NAME}.tar.gz`
const NODEJS_URL = `https://nodejs.org/dist/v${NODEJS_VERSION}/${NODEJS_FILE}`

const onServiceUpCallbacks: ((ip: string, port: number) => Promise<void>)[] = []
export const clearServiceUpCallbacks = () => onServiceUpCallbacks.length = 0
export const setServiceUpCallback = (callback: (ip: string, port: number) => Promise<void>) => onServiceUpCallbacks.push(callback)
Expand Down Expand Up @@ -180,6 +188,30 @@ async function startHostService() {
DIVE_USER_AGENT: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Dive/${packageJson.version} (+https://github.com/OpenAgentPlatform/Dive)`,
}

// Set tool paths for packaged app
if (app.isPackaged) {
// NPX path: Windows and Mac use resourcesPath, Linux uses downloaded path (~/.dive/bin/nodejs)
if (isWindows) {
httpdEnv.TOOL_NPX_PATH = path.join(resourcePath, "node", "npx.cmd")
} else if (process.platform === "darwin") {
httpdEnv.TOOL_NPX_PATH = path.join(resourcePath, "node", "bin", "npx")
} else {
// Linux - uses downloaded nodejs in ~/.dive/bin/nodejs
httpdEnv.TOOL_NPX_PATH = path.join(binDir, "nodejs", "bin", "npx")
}

if (isWindows) {
httpdEnv.TOOL_UVX_PATH = path.join(resourcePath, "uv", "uvx.exe")
} else if (process.platform === "darwin") {
httpdEnv.TOOL_UVX_PATH = path.join(resourcePath, "uv", "uvx")
} else {
httpdEnv.TOOL_UVX_PATH = path.join(resourcePath, "uv", "uvx")
}

console.log(`npx for builtin tool: ${httpdEnv.TOOL_NPX_PATH}`)
console.log(`uvx for builtin tool: ${httpdEnv.TOOL_UVX_PATH}`)
}

console.log("httpd executing path: ", httpdExec)

const busPath = path.join(hostCacheDir, "bus")
Expand Down Expand Up @@ -278,6 +310,79 @@ async function startHostService() {
})
}

async function needToDownloadNodejs(): Promise<boolean> {
if (process.platform !== "linux") {
return false
}

const nodejsDir = path.join(binDir, "nodejs")
const nodePath = path.join(nodejsDir, "bin", "node")

if (!(await fse.pathExists(nodePath))) {
return true
}

try {
const result = spawn.sync(nodePath, ["-v"])
const version = result.stdout?.toString().trim()
return version !== `v${NODEJS_VERSION}`
} catch {
return true
}
}

async function downloadNodejs(win: BrowserWindow): Promise<void> {
// Ensure binDir exists
await mkdirp(binDir)

const nodejsDir = path.join(binDir, "nodejs")
const tmpDir = path.join(binDir, "nodejs_tmp")

// Clean up existing directories
if (await fse.pathExists(nodejsDir)) {
await fse.remove(nodejsDir)
}
await mkdirp(nodejsDir)
await mkdirp(tmpDir)

const nodejsFilePath = path.join(tmpDir, NODEJS_FILE)

console.log(`downloading nodejs from ${NODEJS_URL}`)
installHostDependenciesLog.push(`downloading nodejs from ${NODEJS_URL}`)
win.webContents.send("install-host-dependencies-log", `downloading nodejs from ${NODEJS_URL}`)

// Download the file
const response = await fetch(NODEJS_URL)
if (!response.ok) {
throw new Error(`Failed to download nodejs: ${response.statusText}`)
}

const fileStream = fse.createWriteStream(nodejsFilePath)
// @ts-ignore - response.body is a ReadableStream
await pipeline(response.body, fileStream)

console.log(`extracting nodejs to ${tmpDir}`)
installHostDependenciesLog.push(`extracting nodejs to ${tmpDir}`)
win.webContents.send("install-host-dependencies-log", `extracting nodejs to ${tmpDir}`)

// Extract the tar.gz file using system tar command
await promiseSpawn("tar", ["-xzf", nodejsFilePath, "-C", tmpDir], tmpDir, "pipe")

// Move extracted files to nodejs dir
const extractedDir = path.join(tmpDir, NODEJS_FILE_NAME)
const files = await fse.readdir(extractedDir)
for (const file of files) {
await fse.move(path.join(extractedDir, file), path.join(nodejsDir, file), { overwrite: true })
}

// Clean up
await fse.remove(tmpDir)

console.log("download nodejs done")
installHostDependenciesLog.push("download nodejs done")
win.webContents.send("install-host-dependencies-log", "download nodejs done")
}

async function installHostDependencies(win: BrowserWindow) {
const done = () => {
win.webContents.send("install-host-dependencies-log", "finish")
Expand All @@ -289,6 +394,19 @@ async function installHostDependencies(win: BrowserWindow) {
}

console.log("installing host dependencies")

// Download Node.js for Linux if needed
if (process.platform === "linux") {
try {
if (await needToDownloadNodejs()) {
await downloadNodejs(win)
}
} catch (error) {
console.error("Failed to download nodejs:", error)
installHostDependenciesLog.push(`Failed to download nodejs: ${error}`)
win.webContents.send("install-host-dependencies-log", `Failed to download nodejs: ${error}`)
}
}
const isWindows = process.platform === "win32"
const pyBinPath = path.join(process.resourcesPath, "python", "bin")
const pyPath = isWindows ? path.join(process.resourcesPath, "python", "python.exe") : path.join(pyBinPath, "python3")
Expand Down
Loading