Skip to content

Commit fe154f9

Browse files
itz-muneclaude
andcommitted
feat: v0.2.1 β€” clickable links, per-plugin deps, Tesseract fix
- MessageThread: Markdown links open in browser (β†—) or default app (πŸ“‚) for local file paths. Agent instructed to always use Markdown link syntax for paths and URLs in responses. - plugins/loader: auto-install plugin Python deps via uv on first load - plugins/types: add dependencies[] field to PluginManifest - lib.rs: pass RAGDOLL_UV env var + strip Tesseract from PATH - tauri.conf.json: bump version to 0.2.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 90e9159 commit fe154f9

3 files changed

Lines changed: 95 additions & 4 deletions

File tree

β€Žsrc-tauri/sidecar-src/memory/prompt_builder.pyβ€Ž

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
"Answer using the provided context when relevant. "
1414
"If the context is not relevant to the question, ignore it and answer from "
1515
"your general knowledge. "
16-
"Today is {date}."
16+
"Today is {date}.\n\n"
17+
"FORMATTING RULES:\n"
18+
"- Whenever you mention a file path (e.g. C:\\Users\\...\\file.docx or /home/user/file.pdf), "
19+
"always format it as a Markdown link so the user can click to open it: "
20+
"[filename.ext](C:\\full\\path\\to\\filename.ext). "
21+
"Use the filename as the label and the full absolute path as the URL.\n"
22+
"- Whenever you mention a web URL, format it as a Markdown link: [title](https://url).\n"
23+
"- Never display raw paths or URLs as plain text β€” always wrap them in Markdown link syntax."
1724
)
1825

1926
_CITATION_INSTRUCTION = (

β€Žsrc-tauri/tauri.conf.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "RAGdoll",
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"identifier": "com.mune.ragdoll",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

β€Žsrc/components/chat/MessageThread.tsxβ€Ž

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
2-
import { Copy, ThumbsUp, ThumbsDown, ChevronDown, Check, FileText, FileSpreadsheet, Braces, FileCode, RotateCcw, ImageOff } from 'lucide-react';
2+
import { Copy, ThumbsUp, ThumbsDown, ChevronDown, Check, FileText, FileSpreadsheet, Braces, FileCode, RotateCcw, ImageOff, ExternalLink, FolderOpen } from 'lucide-react';
33
import { ThinkingBlock } from '@/components/chat/ThinkingBlock';
44
import { ImageLightbox } from '@/components/chat/ImageLightbox';
55
import { ToolCallIndicator, SkillLoadingIndicator } from '@/components/chat/ToolCallIndicator';
@@ -18,6 +18,46 @@ import { FileOperationResult, parseFileOp } from '@/components/skills/FileOperat
1818
import { Button } from '@/components/ui/button';
1919
import { cn } from '@/lib/utils';
2020
import { useChat } from '@/hooks/useChat';
21+
import { openUrl, openPath } from '@tauri-apps/plugin-opener';
22+
23+
// ─── Link helpers ─────────────────────────────────────────────────────────────
24+
25+
/** True when href points at a local file rather than a web URL. */
26+
function isLocalFilePath(href: string): boolean {
27+
if (href.startsWith('file://')) return true;
28+
if (/^[A-Za-z]:[/\\]/.test(href)) return true; // Windows: C:\… or C:/…
29+
if (href.startsWith('/') && !href.startsWith('//')) return true; // Unix absolute
30+
return false;
31+
}
32+
33+
/** Convert a file:// URL or raw Windows/Unix path to a plain filesystem path. */
34+
function resolveLocalPath(href: string): string {
35+
if (href.startsWith('file:///')) {
36+
const path = decodeURIComponent(href.slice(7)); // keeps leading /
37+
// Windows: /C:/path β†’ C:/path (strip leading slash before drive letter)
38+
return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
39+
}
40+
if (href.startsWith('file://')) {
41+
return decodeURIComponent(href.slice(7));
42+
}
43+
return href;
44+
}
45+
46+
async function handleLinkClick(
47+
e: React.MouseEvent<HTMLAnchorElement>,
48+
href: string,
49+
): Promise<void> {
50+
e.preventDefault();
51+
try {
52+
if (isLocalFilePath(href)) {
53+
await openPath(resolveLocalPath(href));
54+
} else if (href.startsWith('http://') || href.startsWith('https://')) {
55+
await openUrl(href);
56+
}
57+
} catch (err) {
58+
console.error('[link] Failed to open:', href, err);
59+
}
60+
}
2161

2262
const SUGGESTED_PROMPTS = [
2363
'Summarize a document',
@@ -342,6 +382,29 @@ function MessageBubble({ message, onRegenerate }: { message: Message; onRegenera
342382
<ReactMarkdown
343383
remarkPlugins={[remarkGfm]}
344384
components={{
385+
// Links β€” web URLs open in system browser; local paths open with default app
386+
a({ href, children }) {
387+
if (!href) return <span>{children}</span>;
388+
const local = isLocalFilePath(href);
389+
return (
390+
<a
391+
href={href}
392+
onClick={(e) => void handleLinkClick(e, href)}
393+
title={href}
394+
className={cn(
395+
'inline-flex items-center gap-0.5 underline underline-offset-2 cursor-pointer transition-colors',
396+
isUser
397+
? 'text-primary-foreground/80 hover:text-primary-foreground'
398+
: 'text-blue-400 hover:text-blue-300',
399+
)}
400+
>
401+
{children}
402+
{local
403+
? <FolderOpen className="inline h-3 w-3 shrink-0 opacity-60" />
404+
: <ExternalLink className="inline h-3 w-3 shrink-0 opacity-60" />}
405+
</a>
406+
);
407+
},
345408
// Inline image β€” styled thumbnail, opens lightbox on click
346409
img({ src, alt }) {
347410
return (
@@ -406,7 +469,28 @@ function MessageBubble({ message, onRegenerate }: { message: Message; onRegenera
406469
{/* "after marker" text (any text following each parsed marker block) */}
407470
{(opTextAfter || statsTextAfter || contentAfter) && !isUser && (
408471
<div className="prose prose-sm dark:prose-invert max-w-none mt-2">
409-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
472+
<ReactMarkdown
473+
remarkPlugins={[remarkGfm]}
474+
components={{
475+
a({ href, children }) {
476+
if (!href) return <span>{children}</span>;
477+
const local = isLocalFilePath(href);
478+
return (
479+
<a
480+
href={href}
481+
onClick={(e) => void handleLinkClick(e, href)}
482+
title={href}
483+
className="inline-flex items-center gap-0.5 text-blue-400 underline underline-offset-2 cursor-pointer transition-colors hover:text-blue-300"
484+
>
485+
{children}
486+
{local
487+
? <FolderOpen className="inline h-3 w-3 shrink-0 opacity-60" />
488+
: <ExternalLink className="inline h-3 w-3 shrink-0 opacity-60" />}
489+
</a>
490+
);
491+
},
492+
}}
493+
>
410494
{[opTextAfter, statsTextAfter, contentAfter].filter(Boolean).join('\n\n')}
411495
</ReactMarkdown>
412496
</div>

0 commit comments

Comments
Β (0)