feat(markdown): intercept links in MarkdownPane — relative files navigate in Files tab, external URLs open in browser

This commit is contained in:
Hibryda 2026-03-10 00:58:46 +01:00
parent 91aa711ef3
commit cd438c2cf3
3 changed files with 75 additions and 3 deletions

View file

@ -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 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
// Force dark GTK theme for native dialogs (file chooser, etc.) // Force dark GTK theme for native dialogs (file chooser, etc.)
@ -616,6 +629,7 @@ pub fn run() {
project_agent_state_load, project_agent_state_load,
cli_get_group, cli_get_group,
pick_directory, pick_directory,
open_url,
frontend_log, frontend_log,
]) ])
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())

View file

@ -8,9 +8,10 @@
filePath: string; filePath: string;
paneId: string; paneId: string;
onExit?: () => void; onExit?: () => void;
onNavigate?: (absolutePath: string) => void;
} }
let { filePath, paneId, onExit }: Props = $props(); let { filePath, paneId, onExit, onNavigate }: Props = $props();
let renderedHtml = $state(''); let renderedHtml = $state('');
let error = $state(''); let error = $state('');
@ -72,13 +73,59 @@
unlisten?.(); unlisten?.();
unwatchFile(paneId).catch(() => {}); 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('/');
}
</script> </script>
<div class="markdown-pane"> <div class="markdown-pane">
{#if error} {#if error}
<div class="error">{error}</div> <div class="error">{error}</div>
{:else} {:else}
<div class="markdown-pane-scroll"> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="markdown-pane-scroll" onclick={handleLinkClick}>
<div class="markdown-body"> <div class="markdown-body">
{@html renderedHtml} {@html renderedHtml}
</div> </div>

View file

@ -17,6 +17,17 @@
loadFiles(cwd); 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) { async function loadFiles(dir: string) {
loading = true; loading = true;
try { try {
@ -58,7 +69,7 @@
<main class="doc-content"> <main class="doc-content">
{#if selectedPath} {#if selectedPath}
<MarkdownPane paneId="pf-{projectName}" filePath={selectedPath} /> <MarkdownPane paneId="pf-{projectName}" filePath={selectedPath} onNavigate={handleNavigate} />
{:else} {:else}
<div class="state-msg full">Select a file</div> <div class="state-msg full">Select a file</div>
{/if} {/if}