/** * DshFileMenu — Host half (Cordis plugin body). * * This file is the `code.host` value for cordis_define. It is a plain * JavaScript function body that returns a Cordis plugin: no TypeScript, JSX, * imports or bundler involved. * * Usage inside a DeepSeek Harness session: * 1. cordis_define(kind: "new", idPrefix: "file") with this file as code.host * and plugin/client.js as code.client. * 2. cordis_run to activate; approve the first Client activation in the UI. * * Responsibilities: * - file.open — open a path with the OS default application * - file.reveal — reveal a path in the OS file manager (Explorer/Finder) * - file.editors — detect installed editors + report the host platform * - file.openWith — launch a detected editor with the file * - file.openWithDialog — Windows "Open With" dialog (rundll32) * * All handlers resolve paths through the `fs` service and launch native * commands through the `subprocess` service. The Host sandbox exposes no * `process` global, so the platform is derived from the resolved path shape. */ function dirnameOf(abs) { const at = Math.max(abs.lastIndexOf('/'), abs.lastIndexOf('\\')) return at <= 0 ? '' : abs.slice(0, at) } function isWindowsPath(abs) { return /^[A-Za-z]:[\\/]/.test(abs) || abs.includes('\\') } function readArgs(args) { if (typeof args !== 'object' || args === null) return { path: '.', cwd: undefined, sessionId: undefined } const raw = typeof args.path === 'string' && args.path !== '' ? args.path : '.' const cwd = typeof args.cwd === 'string' && args.cwd !== '' ? args.cwd : undefined const sessionId = typeof args.sessionId === 'string' && args.sessionId !== '' ? args.sessionId : undefined return { path: raw, cwd, sessionId } } function resolveOpts(cwd) { return cwd === undefined ? {} : { cwd } } async function resolveCwd(ctx, cwd, sessionId) { if (typeof cwd === 'string' && cwd !== '') return cwd if (typeof sessionId === 'string' && sessionId !== '') { const sessions = ctx.get('sessions') const session = sessions === undefined ? undefined : sessions.get(sessionId) if (session !== undefined && session.header !== undefined && typeof session.header.cwd === 'string' && session.header.cwd !== '') return session.header.cwd } return undefined } const EDITOR_SPECS = [ { id: 'vscode', name: 'Visual Studio Code', commands: ['code'], exeNames: ['Code.exe'], installs: ['C:\\Program Files\\Microsoft VS Code\\Code.exe', 'C:\\Program Files (x86)\\Microsoft VS Code\\Code.exe'] }, { id: 'cursor', name: 'Cursor', commands: ['cursor'], exeNames: ['Cursor.exe'], installs: [] }, { id: 'windsurf', name: 'Windsurf', commands: ['windsurf'], exeNames: ['Windsurf.exe'], installs: [] }, { id: 'vscodium', name: 'VSCodium', commands: ['codium'], exeNames: ['codium.exe', 'VSCodium.exe'], installs: [] }, { id: 'sublime', name: 'Sublime Text', commands: ['subl'], exeNames: ['sublime_text.exe'], installs: ['C:\\Program Files\\Sublime Text\\sublime_text.exe'] }, { id: 'notepadpp', name: 'Notepad++', commands: [], exeNames: [], installs: ['C:\\Program Files\\Notepad++\\notepad++.exe', 'C:\\Program Files (x86)\\Notepad++\\notepad++.exe'] }, { id: 'typora', name: 'Typora', commands: ['typora'], exeNames: ['Typora.exe'], installs: ['C:\\Program Files\\Typora\\Typora.exe', 'C:\\Program Files (x86)\\Typora\\Typora.exe'] }, { id: 'hbuilderx', name: 'HBuilderX', commands: ['hbuilderx', 'hbuilder'], exeNames: ['HBuilderX.exe'], installs: ['C:\\Program Files\\HBuilderX\\HBuilderX.exe', 'C:\\Program Files (x86)\\HBuilderX\\HBuilderX.exe'] }, { id: 'zed', name: 'Zed', commands: ['zed'], exeNames: ['Zed.exe', 'zed.exe'], installs: ['C:\\Program Files\\Zed\\Zed.exe'] }, { id: 'idea', name: 'IntelliJ IDEA', commands: ['idea', 'idea64'], exeNames: [], installs: [] }, { id: 'webstorm', name: 'WebStorm', commands: ['webstorm', 'webstorm64'], exeNames: [], installs: [] }, { id: 'pycharm', name: 'PyCharm', commands: ['pycharm', 'pycharm64'], exeNames: [], installs: [] }, { id: 'rider', name: 'Rider', commands: ['rider', 'rider64'], exeNames: [], installs: [] }, { id: 'clion', name: 'CLion', commands: ['clion', 'clion64'], exeNames: [], installs: [] }, { id: 'neovim', name: 'Neovim', commands: ['nvim'], exeNames: [], installs: [] }, { id: 'vim', name: 'Vim', commands: ['vim'], exeNames: [], installs: [] }, { id: 'gedit', name: 'gedit', commands: ['gedit'], exeNames: [], installs: [] } ] async function detectEditors(fs, subprocess) { const found = [] for (const spec of EDITOR_SPECS) { let command for (const cmd of spec.commands) { try { const resolved = await subprocess.resolveExecutable(cmd) if (/\.exe$/i.test(resolved)) { command = resolved break } if (/\.(cmd|bat)$/i.test(resolved)) { const installDir = resolved.replace(/[\\/]bin[\\/][^\\/]+$/, '') if (installDir !== resolved) { for (const exe of spec.exeNames) { const candidate = installDir + '\\' + exe try { const info = await fs.lstat(candidate) if (info !== undefined) { command = candidate break } } catch { /* skip */ } } } if (command !== undefined) break } } catch { /* not resolvable */ } } if (command === undefined) { for (const abs of spec.installs) { try { const info = await fs.lstat(abs) if (info !== undefined) { command = abs break } } catch { /* skip */ } } } if (command !== undefined) found.push({ id: spec.id, name: spec.name, command }) } return found } return { apply(ctx) { const fsService = () => ctx.get('fs') const subprocessService = () => ctx.get('subprocess') let editorsCache const trySpawn = async (argv) => { const subprocess = subprocessService() if (subprocess === undefined) return false try { const handle = subprocess.spawn({ argv, stdio: { stdin: 'ignore', stdout: 'ignore', stderr: 'ignore' }, graceMs: 2000 }) await handle.done return true } catch (error) { console.error('[filemenu] spawn failed:', argv, error) return false } } const getEditors = async () => { if (editorsCache !== undefined) return editorsCache const fs = fsService() const subprocess = subprocessService() if (fs === undefined || subprocess === undefined) return [] try { editorsCache = await detectEditors(fs, subprocess) return editorsCache } catch { return [] } } const platformOf = async () => { const fs = fsService() if (fs === undefined) return 'unknown' try { const target = await fs.resolve('.', {}) return isWindowsPath(fs.processPath(target)) ? 'windows' : 'posix' } catch { return 'unknown' } } harness.handle('file.open', async (args) => { const fs = fsService() const subprocess = subprocessService() if (fs === undefined || subprocess === undefined) return { ok: false, error: 'fs or subprocess service unavailable' } try { const { path, cwd, sessionId } = readArgs(args) const resolvedCwd = await resolveCwd(ctx, cwd, sessionId) const target = await fs.resolve(path, resolveOpts(resolvedCwd)) const abs = fs.processPath(target) if (isWindowsPath(abs)) { const lit = abs.replace(/'/g, "''") if (await trySpawn(['powershell.exe', '-NoProfile', '-Command', "Invoke-Item -LiteralPath '" + lit + "'"])) return { ok: true } return { ok: false, error: 'failed to open' } } if ((await trySpawn(['open', abs]))) return { ok: true } if ((await trySpawn(['xdg-open', abs]))) return { ok: true } return { ok: false, error: 'no native opener available' } } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) } } }) harness.handle('file.reveal', async (args) => { const fs = fsService() const subprocess = subprocessService() if (fs === undefined || subprocess === undefined) return { ok: false, error: 'fs or subprocess service unavailable' } try { const { path, cwd, sessionId } = readArgs(args) const resolvedCwd = await resolveCwd(ctx, cwd, sessionId) const target = await fs.resolve(path, resolveOpts(resolvedCwd)) const abs = fs.processPath(target) const info = await fs.stat(target) let revealAbs = abs let isDir = false if (info === undefined) { const parent = dirnameOf(abs) if (parent === '') return { ok: false, error: 'cannot reveal "' + abs + '"' } revealAbs = parent } else { try { await fs.listDir(target) isDir = true } catch { isDir = false } } if (isWindowsPath(revealAbs)) { const argv = isDir ? ['explorer.exe', revealAbs] : ['explorer.exe', '/select,' + revealAbs] if (await trySpawn(argv)) return { ok: true } return { ok: false, error: 'failed to start explorer' } } const parent = dirnameOf(revealAbs) if (!isDir && (await trySpawn(['open', '-R', revealAbs]))) return { ok: true } if ((await trySpawn(['open', revealAbs]))) return { ok: true } if (parent !== '' && (await trySpawn(['xdg-open', parent]))) return { ok: true } if ((await trySpawn(['xdg-open', revealAbs]))) return { ok: true } return { ok: false, error: 'no native reveal opener available' } } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) } } }) harness.handle('file.editors', async () => { return { platform: await platformOf(), editors: await getEditors() } }) harness.handle('file.openWith', async (args) => { const fs = fsService() const subprocess = subprocessService() if (fs === undefined || subprocess === undefined) return { ok: false, error: 'fs or subprocess service unavailable' } try { const { path, cwd, sessionId } = readArgs(args) const editorId = typeof args === 'object' && args !== null && typeof args.editorId === 'string' ? args.editorId : undefined if (editorId === undefined) return { ok: false, error: 'editorId is required' } const resolvedCwd = await resolveCwd(ctx, cwd, sessionId) const target = await fs.resolve(path, resolveOpts(resolvedCwd)) const abs = fs.processPath(target) const editors = await getEditors() const editor = editors.find((entry) => entry.id === editorId) if (editor === undefined) return { ok: false, error: 'editor not found: ' + editorId } if (await trySpawn([editor.command, abs])) return { ok: true } return { ok: false, error: 'failed to launch editor' } } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) } } }) harness.handle('file.openWithDialog', async (args) => { const fs = fsService() const subprocess = subprocessService() if (fs === undefined || subprocess === undefined) return { ok: false, error: 'fs or subprocess service unavailable' } try { const { path, cwd, sessionId } = readArgs(args) const resolvedCwd = await resolveCwd(ctx, cwd, sessionId) const target = await fs.resolve(path, resolveOpts(resolvedCwd)) const abs = fs.processPath(target) if (!isWindowsPath(abs)) return { ok: false, unsupported: true, error: 'Open With dialog is Windows-only' } if (await trySpawn(['rundll32.exe', 'shell32.dll,OpenAs_RunDLL', abs])) return { ok: true } return { ok: false, error: 'failed to open the dialog' } } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) } } }) }, }