From cd438c2cf3143a2380ab972d348f2d90b25c8d51 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Tue, 10 Mar 2026 00:58:46 +0100 Subject: [PATCH] =?UTF-8?q?feat(markdown):=20intercept=20links=20in=20Mark?= =?UTF-8?q?downPane=20=E2=80=94=20relative=20files=20navigate=20in=20Files?= =?UTF-8?q?=20tab,=20external=20URLs=20open=20in=20browser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- v2/src-tauri/src/lib.rs | 14 +++++ .../components/Markdown/MarkdownPane.svelte | 51 ++++++++++++++++++- .../components/Workspace/ProjectFiles.svelte | 13 ++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 3100bc7..91f08cd 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -552,6 +552,19 @@ async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: Str state.remote_manager.pty_kill(&machine_id, &id).await } +#[tauri::command] +fn open_url(url: String) -> Result<(), String> { + // Only allow http/https URLs + if !url.starts_with("http://") && !url.starts_with("https://") { + return Err("Only http/https URLs are allowed".into()); + } + std::process::Command::new("xdg-open") + .arg(&url) + .spawn() + .map_err(|e| format!("Failed to open URL: {e}"))?; + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Force dark GTK theme for native dialogs (file chooser, etc.) @@ -616,6 +629,7 @@ pub fn run() { project_agent_state_load, cli_get_group, pick_directory, + open_url, frontend_log, ]) .plugin(tauri_plugin_updater::Builder::new().build()) diff --git a/v2/src/lib/components/Markdown/MarkdownPane.svelte b/v2/src/lib/components/Markdown/MarkdownPane.svelte index 45874df..0d03a03 100644 --- a/v2/src/lib/components/Markdown/MarkdownPane.svelte +++ b/v2/src/lib/components/Markdown/MarkdownPane.svelte @@ -8,9 +8,10 @@ filePath: string; paneId: string; onExit?: () => void; + onNavigate?: (absolutePath: string) => void; } - let { filePath, paneId, onExit }: Props = $props(); + let { filePath, paneId, onExit, onNavigate }: Props = $props(); let renderedHtml = $state(''); let error = $state(''); @@ -72,13 +73,59 @@ unlisten?.(); unwatchFile(paneId).catch(() => {}); }); + + function handleLinkClick(event: MouseEvent) { + const anchor = (event.target as HTMLElement).closest('a'); + if (!anchor) return; + + const href = anchor.getAttribute('href'); + if (!href) return; + + // Anchor links — scroll within page + if (href.startsWith('#')) return; + + event.preventDefault(); + + // External URLs — open in system browser + if (/^https?:\/\//.test(href)) { + import('@tauri-apps/api/core').then(({ invoke }) => { + invoke('open_url', { url: href }).catch(() => { + // Fallback: do nothing (no shell plugin) + }); + }); + return; + } + + // Relative file link — resolve against current file's directory + if (onNavigate) { + const dir = filePath.replace(/\/[^/]*$/, ''); + const resolved = resolveRelativePath(dir, href); + onNavigate(resolved); + } + } + + function resolveRelativePath(base: string, relative: string): string { + // Strip any anchor or query from the link + const cleanRelative = relative.split('#')[0].split('?')[0]; + const parts = base.split('/'); + for (const segment of cleanRelative.split('/')) { + if (segment === '..') { + parts.pop(); + } else if (segment !== '.' && segment !== '') { + parts.push(segment); + } + } + return parts.join('/'); + }
{#if error}
{error}
{:else} -
+ + +
{@html renderedHtml}
diff --git a/v2/src/lib/components/Workspace/ProjectFiles.svelte b/v2/src/lib/components/Workspace/ProjectFiles.svelte index 8329e3a..1d3d806 100644 --- a/v2/src/lib/components/Workspace/ProjectFiles.svelte +++ b/v2/src/lib/components/Workspace/ProjectFiles.svelte @@ -17,6 +17,17 @@ loadFiles(cwd); }); + function handleNavigate(absolutePath: string) { + // If the file is in our discovered list, select it directly + const match = files.find(f => f.path === absolutePath); + if (match) { + selectedPath = absolutePath; + } else { + // File not in sidebar — set it directly (MarkdownPane handles loading) + selectedPath = absolutePath; + } + } + async function loadFiles(dir: string) { loading = true; try { @@ -58,7 +69,7 @@
{#if selectedPath} - + {:else}
Select a file
{/if}