diff --git a/apps/web/app/api/workspace/open-file/route.ts b/apps/web/app/api/workspace/open-file/route.ts index 6da26fc8351..402f0e2f527 100644 --- a/apps/web/app/api/workspace/open-file/route.ts +++ b/apps/web/app/api/workspace/open-file/route.ts @@ -35,7 +35,24 @@ export async function POST(req: Request) { ? rawPath.replace(/^~/, homedir()) : rawPath; - const resolved = resolve(normalize(expanded)); + let resolved = resolve(normalize(expanded)); + + // If the file doesn't exist and looks like a bare filename, try to locate it + // using macOS Spotlight (mdfind). + if (!existsSync(resolved) && !rawPath.includes("/")) { + const found = await new Promise((res) => { + exec( + `mdfind -name ${JSON.stringify(rawPath)} | head -1`, + (err, stdout) => { + if (err || !stdout.trim()) {res(null);} + else {res(stdout.trim().split("\n")[0]);} + }, + ); + }); + if (found && existsSync(found)) { + resolved = found; + } + } if (!existsSync(resolved)) { return Response.json( diff --git a/apps/web/app/api/workspace/path-info/route.ts b/apps/web/app/api/workspace/path-info/route.ts index 3742d462e08..e06c0e60469 100644 --- a/apps/web/app/api/workspace/path-info/route.ts +++ b/apps/web/app/api/workspace/path-info/route.ts @@ -1,3 +1,4 @@ +import { exec } from "node:child_process"; import { existsSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { basename, normalize, resolve } from "node:path"; @@ -39,7 +40,24 @@ export async function GET(req: Request) { const expandedPath = candidatePath.startsWith("~/") ? candidatePath.replace(/^~/, homedir()) : candidatePath; - const resolvedPath = resolve(normalize(expandedPath)); + let resolvedPath = resolve(normalize(expandedPath)); + + // If the path doesn't exist and looks like a bare filename, try to locate it + // using macOS Spotlight (mdfind). + if (!existsSync(resolvedPath) && !rawPath.includes("/")) { + const found = await new Promise((res) => { + exec( + `mdfind -name ${JSON.stringify(rawPath)} | head -1`, + (err, stdout) => { + if (err || !stdout.trim()) {res(null);} + else {res(stdout.trim().split("\n")[0]);} + }, + ); + }); + if (found && existsSync(found)) { + resolvedPath = found; + } + } if (!existsSync(resolvedPath)) { return Response.json( diff --git a/apps/web/app/components/chat-message.tsx b/apps/web/app/components/chat-message.tsx index 35b75847026..27c955709f7 100644 --- a/apps/web/app/components/chat-message.tsx +++ b/apps/web/app/components/chat-message.tsx @@ -452,17 +452,28 @@ function AttachedFilesCard({ paths }: { paths: string[] }) { function looksLikeFilePath(text: string): boolean { const t = text.trim(); if (!t || t.length < 3 || t.length > 500) {return false;} - // Must start with a path prefix - if (!(t.startsWith("~/") || t.startsWith("/") || t.startsWith("./") || t.startsWith("../"))) { - return false; + // Full path prefix + if (t.startsWith("~/") || t.startsWith("/") || t.startsWith("./") || t.startsWith("../")) { + const afterPrefix = t.startsWith("~/") ? t.slice(2) : + t.startsWith("../") ? t.slice(3) : + t.startsWith("./") ? t.slice(2) : + t.slice(1); + return afterPrefix.includes("/") || afterPrefix.includes("."); } - // Must have at least one path separator beyond the prefix - // (avoids matching bare `/` or standalone commands like `/bin`) - const afterPrefix = t.startsWith("~/") ? t.slice(2) : - t.startsWith("../") ? t.slice(3) : - t.startsWith("./") ? t.slice(2) : - t.slice(1); - return afterPrefix.includes("/") || afterPrefix.includes("."); + // Bare filename with a known extension (e.g. "Rachapoom-Passport.pdf") + const fileExtPattern = /\.(pdf|docx?|xlsx?|pptx?|csv|txt|rtf|pages|numbers|key|md|json|yaml|yml|toml|xml|html?|css|jsx?|tsx?|py|rb|go|rs|java|cpp|c|h|sh|sql|swift|kt|png|jpe?g|gif|webp|svg|bmp|ico|heic|tiff|mp[34]|webm|mov|avi|mkv|flv|wav|ogg|aac|flac|m4a|zip|tar|gz|dmg)$/i; + if (fileExtPattern.test(t) && !t.includes(" ")) { + return true; + } + return false; +} + +/** Check if text looks like a filename (allows spaces, used for bold text). */ +function looksLikeFileName(text: string): boolean { + const t = text.trim(); + if (!t || t.length < 3 || t.length > 300) {return false;} + const fileExtPattern = /\.(pdf|docx?|xlsx?|pptx?|csv|txt|rtf|pages|numbers|key|md|json|yaml|yml|toml|xml|html?|css|jsx?|tsx?|py|rb|go|rs|java|cpp|c|h|sh|sql|swift|kt|png|jpe?g|gif|webp|svg|bmp|ico|heic|tiff|mp[34]|webm|mov|avi|mkv|flv|wav|ogg|aac|flac|m4a|zip|tar|gz|dmg)$/i; + return fileExtPattern.test(t); } /** Open a file path using the system default application. */ @@ -559,7 +570,8 @@ function FilePathCode({ return ( - - {status === "error" ? ( - <> - - - - - ) : ( - <> - - - - )} - {children} ); @@ -711,6 +699,22 @@ function createMarkdownComponents( // Regular inline code return {children}; }, + // Bold text — detect filenames and make them clickable + strong: ({ children, ...props }) => { + const text = typeof children === "string" ? children + : Array.isArray(children) ? children.filter((c) => typeof c === "string").join("") + : ""; + if (text && looksLikeFileName(text)) { + return ( + + + {children} + + + ); + } + return {children}; + }, }; } diff --git a/apps/web/app/components/chat-panel.tsx b/apps/web/app/components/chat-panel.tsx index 69c322dad99..8f225d9b4f9 100644 --- a/apps/web/app/components/chat-panel.tsx +++ b/apps/web/app/components/chat-panel.tsx @@ -674,6 +674,7 @@ export const ChatPanel = forwardRef( // ── Message queue (messages to send after current run completes) ── const [queuedMessages, setQueuedMessages] = useState([]); + const [rawView, setRawView] = useState(false); const filePath = fileContext?.path ?? null; @@ -1548,6 +1549,12 @@ export const ChatPanel = forwardRef( + setRawView((v) => !v)} + > + + {rawView ? "Rendered view" : "Raw view"} + onDeleteSession(currentSessionId)} @@ -1692,7 +1699,14 @@ export const ChatPanel = forwardRef(
- {messages.map((message, i) => ( + {rawView ? ( +
+								{JSON.stringify(messages, null, 2)}
+							
+ ) : messages.map((message, i) => ( {/* Scrollable list fills the sidebar; header overlays the top with blur */} @@ -414,7 +414,7 @@ export function ChatSessionsSidebar({ style={{ height: headerHeight, borderColor: "var(--color-border)", - background: "color-mix(in srgb, var(--color-surface) 80%, transparent)", + background: "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)", }} >
diff --git a/apps/web/app/components/workspace/file-manager-tree.tsx b/apps/web/app/components/workspace/file-manager-tree.tsx index 94bf8aaec19..8f1104f537f 100644 --- a/apps/web/app/components/workspace/file-manager-tree.tsx +++ b/apps/web/app/components/workspace/file-manager-tree.tsx @@ -81,14 +81,15 @@ function isSystemFile(path: string): boolean { // --- Icons (inline SVG, zero-dep) --- function FolderIcon({ open }: { open?: boolean }) { - return open ? ( - - - - ) : ( - - - + return ( + ); } @@ -110,17 +111,13 @@ function KanbanIcon() { function DocumentIcon() { return ( - - - + ); } function FileIcon() { return ( - - - + ); } @@ -548,7 +545,7 @@ function DraggableNode({ onCancel={onCancelRename} /> ) : ( - {node.name.replace(/\.md$/, "")} + {node.name} )} {/* Workspace badge for the workspace root entry point */} diff --git a/apps/web/app/components/workspace/workspace-sidebar.tsx b/apps/web/app/components/workspace/workspace-sidebar.tsx index d68bddec5a4..c7e66b751a4 100644 --- a/apps/web/app/components/workspace/workspace-sidebar.tsx +++ b/apps/web/app/components/workspace/workspace-sidebar.tsx @@ -68,18 +68,14 @@ function HomeIcon() { function FolderOpenIcon() { return ( - - - + ); } @@ -173,25 +169,26 @@ function SearchIcon() { function SmallFolderIcon() { return ( - - - + ); } function SmallFileIcon() { return ( - - - + ); } function SmallDocIcon() { return ( - - - + ); } @@ -417,13 +414,13 @@ export function WorkspaceSidebar({ style={{ width: typeof width === "number" ? `${width}px` : width, minWidth: typeof width === "number" ? `${width}px` : width, - background: "var(--color-surface)", + background: "var(--color-sidebar-bg)", borderColor: "var(--color-border)", }} > {/* Header */}
{isBrowsing ? ( @@ -473,16 +470,14 @@ export function WorkspaceSidebar({