From 74445998178233222f4d78039be083061b54d651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Tue, 17 Mar 2026 18:20:37 +1100 Subject: [PATCH] fix: harden external link navigation in the web app - add a shared external-navigation guard at the document level - route onboarding, markdown, settings, and node source links through one helper - stop browser fallback code from replacing the current app page on failures Generated with Codex --- app/layout.tsx | 2 + src/components/focus/FocusPanel.tsx | 36 ++++++++++-- .../helpers/MarkdownWithNodeTokens.tsx | 14 ++++- src/components/onboarding/FirstRunModal.tsx | 20 +++++-- src/components/settings/ApiKeysViewer.tsx | 20 +++++-- src/components/settings/DatabaseViewer.tsx | 8 ++- .../system/ExternalNavigationManager.tsx | 48 ++++++++++++++++ src/utils/openExternalUrl.ts | 57 +++++++++++-------- 8 files changed, 164 insertions(+), 41 deletions(-) create mode 100644 src/components/system/ExternalNavigationManager.tsx diff --git a/app/layout.tsx b/app/layout.tsx index b0b3e38..bbad70a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import './globals.css'; import { DimensionIconsProvider } from '@/context/DimensionIconsContext'; +import ExternalNavigationManager from '@/components/system/ExternalNavigationManager'; export const metadata = { title: 'RA-H Open Source', @@ -15,6 +16,7 @@ export default function RootLayout({ + {children} diff --git a/src/components/focus/FocusPanel.tsx b/src/components/focus/FocusPanel.tsx index f2820c5..7750f7a 100644 --- a/src/components/focus/FocusPanel.tsx +++ b/src/components/focus/FocusPanel.tsx @@ -9,6 +9,7 @@ import { parseNodeMarkers } from '@/tools/infrastructure/nodeFormatter'; import { Node, NodeConnection, Chunk } from '@/types/database'; import DimensionTags from './dimensions/DimensionTags'; import { getNodeIcon } from '@/utils/nodeIcons'; +import { openExternalUrl, shouldOpenExternally } from '@/utils/openExternalUrl'; import { useDimensionIcons } from '@/context/DimensionIconsContext'; import ConfirmDialog from '../common/ConfirmDialog'; import { SourceReader } from './source'; @@ -1811,13 +1812,23 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli ) : nodesData[activeTab].link ? ( { if (e.metaKey || e.ctrlKey) { e.preventDefault(); startEdit('link', nodesData[activeTab].link || ''); + return; } + + const link = nodesData[activeTab].link; + if (!link || !shouldOpenExternally(link)) { + return; + } + + e.preventDefault(); + void openExternalUrl(link).catch((error) => { + console.error('[FocusPanel] Failed to open node link', error); + window.alert(`Unable to open ${link}`); + }); }} style={{ color: '#3b82f6', @@ -1993,9 +2004,26 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli title="Connections" > - {activeTab && edgesData[activeTab] && edgesData[activeTab].length > 0 && ( + Edges + {activeTab && ( - {edgesData[activeTab].length} + + {(edgesData[activeTab] || []).length} + )} diff --git a/src/components/helpers/MarkdownWithNodeTokens.tsx b/src/components/helpers/MarkdownWithNodeTokens.tsx index caa4c0d..7acde2c 100644 --- a/src/components/helpers/MarkdownWithNodeTokens.tsx +++ b/src/components/helpers/MarkdownWithNodeTokens.tsx @@ -3,6 +3,7 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { openExternalUrl, shouldOpenExternally } from '@/utils/openExternalUrl'; interface NodeLabelInlineProps { id: string; @@ -197,8 +198,17 @@ export default function MarkdownWithNodeTokens({ content, onNodeClick }: Markdow a: ({ href, children }) => ( { + if (!href || !shouldOpenExternally(href)) { + return; + } + + event.preventDefault(); + void openExternalUrl(href).catch((error) => { + console.error('[MarkdownWithNodeTokens] Failed to open external link', error); + window.alert(`Unable to open ${href}`); + }); + }} style={{ color: '#22c55e', textDecoration: 'underline' }} > {processChildren(children, 'a')} diff --git a/src/components/onboarding/FirstRunModal.tsx b/src/components/onboarding/FirstRunModal.tsx index aa08dd8..82b4c5d 100644 --- a/src/components/onboarding/FirstRunModal.tsx +++ b/src/components/onboarding/FirstRunModal.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, type CSSProperties } from 'react'; import { createPortal } from 'react-dom'; import { isFirstRun, markFirstRunComplete } from '@/services/storage/apiKeys'; +import { openExternalUrl } from '@/utils/openExternalUrl'; export default function FirstRunModal() { const [isOpen, setIsOpen] = useState(false); @@ -46,14 +47,18 @@ export default function FirstRunModal() {

Without a key, you can still create and organise nodes manually.

- { + void openExternalUrl('https://platform.openai.com/api-keys').catch((error) => { + console.error('[FirstRunModal] Failed to open OpenAI API keys page', error); + window.alert('Unable to open the OpenAI API keys page automatically.'); + }); + }} style={linkStyle} > Get an API key from OpenAI → - +

@@ -148,4 +153,9 @@ const noteStyle: CSSProperties = { const linkStyle: CSSProperties = { color: '#22c55e', textDecoration: 'none', + background: 'transparent', + border: 'none', + padding: 0, + cursor: 'pointer', + font: 'inherit', }; diff --git a/src/components/settings/ApiKeysViewer.tsx b/src/components/settings/ApiKeysViewer.tsx index c0bb966..0ac93b3 100644 --- a/src/components/settings/ApiKeysViewer.tsx +++ b/src/components/settings/ApiKeysViewer.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, type CSSProperties } from 'react'; +import { openExternalUrl } from '@/utils/openExternalUrl'; export default function ApiKeysViewer() { const [status, setStatus] = useState<'checking' | 'configured' | 'not-set'>('checking'); @@ -59,14 +60,18 @@ export default function ApiKeysViewer() { {/* Get key link */}
- { + void openExternalUrl('https://platform.openai.com/api-keys').catch((error) => { + console.error('[ApiKeysViewer] Failed to open OpenAI API keys page', error); + window.alert('Unable to open the OpenAI API keys page automatically.'); + }); + }} style={linkStyle} > Get your API key from OpenAI → - +
); @@ -163,4 +168,9 @@ const helpStyle: CSSProperties = { const linkStyle: CSSProperties = { color: '#22c55e', textDecoration: 'none', + background: 'transparent', + border: 'none', + padding: 0, + cursor: 'pointer', + font: 'inherit', }; diff --git a/src/components/settings/DatabaseViewer.tsx b/src/components/settings/DatabaseViewer.tsx index d9742a0..e45fb88 100644 --- a/src/components/settings/DatabaseViewer.tsx +++ b/src/components/settings/DatabaseViewer.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type KeyboardEvent } from 'react'; import { Folder, Link as LinkIcon } from 'lucide-react'; import { Node } from '@/types/database'; +import { openExternalUrl } from '@/utils/openExternalUrl'; interface NodeWithMetrics extends Node { edge_count?: number; @@ -440,7 +441,12 @@ export default function DatabaseViewer() { {node.title || 'Untitled node'} {node.link && (