mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-29 11:42:04 +00:00
147 lines
3.7 KiB
TypeScript
147 lines
3.7 KiB
TypeScript
import { isDesktopFsRemoteMode, readDesktopFileText } from '@/lib/desktop-fs'
|
|
import type { PreviewTarget } from '@/store/preview'
|
|
|
|
const HTML_EXTENSIONS = new Set(['.htm', '.html'])
|
|
const IMAGE_EXTENSIONS = new Set(['.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'])
|
|
|
|
const LANGUAGE_BY_EXT: Record<string, string> = {
|
|
'.c': 'c',
|
|
'.conf': 'ini',
|
|
'.cpp': 'cpp',
|
|
'.css': 'css',
|
|
'.csv': 'csv',
|
|
'.go': 'go',
|
|
'.graphql': 'graphql',
|
|
'.h': 'c',
|
|
'.hpp': 'cpp',
|
|
'.html': 'html',
|
|
'.java': 'java',
|
|
'.js': 'javascript',
|
|
'.json': 'json',
|
|
'.jsx': 'jsx',
|
|
'.log': 'text',
|
|
'.lua': 'lua',
|
|
'.md': 'markdown',
|
|
'.mjs': 'javascript',
|
|
'.py': 'python',
|
|
'.rb': 'ruby',
|
|
'.rs': 'rust',
|
|
'.sh': 'shell',
|
|
'.sql': 'sql',
|
|
'.svg': 'xml',
|
|
'.toml': 'toml',
|
|
'.ts': 'typescript',
|
|
'.tsx': 'tsx',
|
|
'.txt': 'text',
|
|
'.xml': 'xml',
|
|
'.yaml': 'yaml',
|
|
'.yml': 'yaml',
|
|
'.zsh': 'shell'
|
|
}
|
|
|
|
function basename(value: string) {
|
|
return value.split(/[\\/]/).filter(Boolean).pop() || value
|
|
}
|
|
|
|
function extension(value: string) {
|
|
const clean = value.split(/[?#]/, 1)[0] || value
|
|
const idx = clean.lastIndexOf('.')
|
|
|
|
return idx >= 0 ? clean.slice(idx).toLowerCase() : ''
|
|
}
|
|
|
|
function joinPath(base: string, rel: string) {
|
|
if (!base) {
|
|
return rel
|
|
}
|
|
|
|
return `${base.replace(/\/+$/, '')}/${rel.replace(/^\.?\//, '')}`
|
|
}
|
|
|
|
function pathToFileUrl(path: string) {
|
|
const encoded = path
|
|
.split('/')
|
|
.map(part => encodeURIComponent(part))
|
|
.join('/')
|
|
|
|
return `file://${encoded.startsWith('/') ? encoded : `/${encoded}`}`
|
|
}
|
|
|
|
export function localPreviewTarget(rawTarget: string, cwd?: string | null): PreviewTarget | null {
|
|
const raw = rawTarget.trim().replace(/^`|`$/g, '')
|
|
|
|
if (!raw) {
|
|
return null
|
|
}
|
|
|
|
if (/^https?:\/\//i.test(raw)) {
|
|
return { kind: 'url', label: basename(raw), source: raw, url: raw }
|
|
}
|
|
|
|
let path = raw
|
|
|
|
if (/^file:\/\//i.test(raw)) {
|
|
try {
|
|
path = decodeURIComponent(new URL(raw).pathname)
|
|
} catch {
|
|
path = raw.replace(/^file:\/\//i, '')
|
|
}
|
|
} else if (!raw.startsWith('/') && cwd) {
|
|
path = joinPath(cwd, raw)
|
|
}
|
|
|
|
const ext = extension(path)
|
|
const isHtml = HTML_EXTENSIONS.has(ext)
|
|
const isImage = IMAGE_EXTENSIONS.has(ext)
|
|
|
|
return {
|
|
kind: 'file',
|
|
label: basename(path),
|
|
language: LANGUAGE_BY_EXT[ext] || 'text',
|
|
path,
|
|
// Renderer fallback can't stat/sniff without reading; assume text unless
|
|
// image/html extension says otherwise. LocalFilePreview still guards
|
|
// binary/large files when readFileText/readFileDataUrl returns metadata.
|
|
previewKind: isHtml ? 'html' : isImage ? 'image' : 'text',
|
|
source: raw,
|
|
url: pathToFileUrl(path)
|
|
}
|
|
}
|
|
|
|
async function enrichPreviewTarget(target: PreviewTarget | null): Promise<PreviewTarget | null> {
|
|
if (!isDesktopFsRemoteMode() || !target || target.kind !== 'file' || target.previewKind === 'image') {
|
|
return target
|
|
}
|
|
|
|
try {
|
|
const result = await readDesktopFileText(target.path || target.source)
|
|
return {
|
|
...target,
|
|
binary: result.binary,
|
|
byteSize: result.byteSize,
|
|
language: result.language || target.language,
|
|
large: (result.byteSize ?? 0) > 512 * 1024,
|
|
mimeType: result.mimeType
|
|
}
|
|
} catch {
|
|
return target
|
|
}
|
|
}
|
|
|
|
export async function normalizeOrLocalPreviewTarget(
|
|
rawTarget: string,
|
|
cwd?: string | null
|
|
): Promise<PreviewTarget | null> {
|
|
try {
|
|
const normalized = await window.hermesDesktop?.normalizePreviewTarget?.(rawTarget, cwd || undefined)
|
|
|
|
if (normalized) {
|
|
return enrichPreviewTarget(normalized)
|
|
}
|
|
} catch {
|
|
// Running Electron may still have the old HTML-only preview IPC. Fall
|
|
// through to renderer-side local classification so text/images still open.
|
|
}
|
|
|
|
return enrichPreviewTarget(localPreviewTarget(rawTarget, cwd))
|
|
}
|