From 2c94e8070ffff890d764581308321b6a343b8f37 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 27 Mar 2026 13:07:14 +0700 Subject: [PATCH] fix(desktop): image rendering for Windows paths and MEDIA: prefix - toFileUrl: normalize backslashes, detect Windows drive-letter paths - resolveFileUrl: strip MEDIA: prefix from tool results, Windows path support - handleServe: skip prepending "/" for Windows drive-letter URL paths --- internal/http/files.go | 8 +++++++- .../src/components/chat/MarkdownRenderer.tsx | 16 ++++++++++++---- ui/desktop/frontend/src/hooks/use-chat.ts | 12 +++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/internal/http/files.go b/internal/http/files.go index b16f6981..019bc1d5 100644 --- a/internal/http/files.go +++ b/internal/http/files.go @@ -107,7 +107,13 @@ func (h *FilesHandler) handleServe(w http.ResponseWriter, r *http.Request) { } // URL path is the absolute path with leading "/" stripped (e.g. "app/.goclaw/workspace/file.png") - absPath := filepath.Clean("/" + urlPath) + // Windows drive letter: "C:/Users/..." → use directly without prepending "/" + var absPath string + if len(urlPath) >= 2 && urlPath[1] == ':' { + absPath = filepath.Clean(urlPath) + } else { + absPath = filepath.Clean("/" + urlPath) + } // Block access to sensitive system directories for _, prefix := range deniedFilePrefixes { diff --git a/ui/desktop/frontend/src/components/chat/MarkdownRenderer.tsx b/ui/desktop/frontend/src/components/chat/MarkdownRenderer.tsx index 4dd76687..9b445af9 100644 --- a/ui/desktop/frontend/src/components/chat/MarkdownRenderer.tsx +++ b/ui/desktop/frontend/src/components/chat/MarkdownRenderer.tsx @@ -28,13 +28,21 @@ function resolveFileUrl(path: string, baseUrl?: string): string { if (isApiClientReady()) return getApiClient().getBaseUrl() + path return path } + // Strip MEDIA: prefix (tool results embed paths as MEDIA:/path/to/file.png) + const clean = path.replace(/^MEDIA:/, '') + // Normalize backslashes (Windows paths: C:\Users\... → C:/Users/...) + const normalized = clean.replace(/\\/g, '/') // Relative path with baseUrl — resolve against the base directory - if (baseUrl && !path.startsWith('/')) { + if (baseUrl && !normalized.startsWith('/') && !/^[a-zA-Z]:\//.test(normalized)) { const baseDir = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1) - return baseDir + path + return baseDir + normalized } - // Absolute path — preserve full path for direct file serving - const filePath = path.startsWith('/') ? `/v1/files${path}` : `/v1/files/${path.split('/').pop() ?? path}` + // Absolute path: Unix /... or Windows C:/... + const filePath = normalized.startsWith('/') + ? `/v1/files${normalized}` + : /^[a-zA-Z]:\//.test(normalized) + ? `/v1/files/${normalized}` + : `/v1/files/${normalized.split('/').pop() ?? normalized}` if (isApiClientReady()) return getApiClient().getBaseUrl() + filePath return filePath } diff --git a/ui/desktop/frontend/src/hooks/use-chat.ts b/ui/desktop/frontend/src/hooks/use-chat.ts index f0f18cd1..50a98586 100644 --- a/ui/desktop/frontend/src/hooks/use-chat.ts +++ b/ui/desktop/frontend/src/hooks/use-chat.ts @@ -10,11 +10,13 @@ import type { AttachedFile } from '../components/chat/InputBar' // can resolve the file directly without findInWorkspace fallback. function toFileUrl(path: string): string { if (!path) return '' - if (path.startsWith('/v1/files/') || path.includes('/v1/files/')) return resolveBase(path) - // Absolute path — preserve full path for direct file serving - if (path.startsWith('/')) return resolveBase(`/v1/files${path}`) - const basename = path.split('/').pop() ?? path - return resolveBase(`/v1/files/${basename}`) + if (path.includes('/v1/files/')) return resolveBase(path) + // Normalize backslashes (Windows paths: C:\Users\... → C:/Users/...) + const normalized = path.replace(/\\/g, '/') + // Absolute: Unix /... or Windows C:/... + if (normalized.startsWith('/')) return resolveBase(`/v1/files${normalized}`) + if (/^[a-zA-Z]:\//.test(normalized)) return resolveBase(`/v1/files/${normalized}`) + return resolveBase(`/v1/files/${normalized.split('/').pop() ?? normalized}`) } function resolveBase(path: string): string {