@@ -134,7 +134,7 @@ export function CollapsibleContent({ content, markdown, defaultCollapsed = false
134134 const [ isOverflow , setIsOverflow ] = useState ( false ) ;
135135 const contentRef = useRef < HTMLDivElement > ( null ) ;
136136
137- const pathHits = usePathHits ( content , cwd ) ;
137+ const pathHits = usePathHits ( content , cwd , markdown ) ;
138138
139139 const rehypePlugins = useMemo ( ( ) => {
140140 const plugins : unknown [ ] = [ ] ;
@@ -161,7 +161,7 @@ export function CollapsibleContent({ content, markdown, defaultCollapsed = false
161161 className = { `text-ink text-sm leading-relaxed ${ collapsed ? "overflow-hidden max-h-10" : "" } ` }
162162 >
163163 { markdown ? (
164- < div className = "prose prose-sm max-w-none prose-p:my-1 prose-headings:my-2 prose-pre:my-0 prose-ul:my-1 prose-ol:my-1 prose-table:my-0 prose-th:px-2 prose-th:py-1 prose-td:px-2 prose-td:py-1 prose-th:border prose-td:border prose-th:border-border prose-td:border-border prose-table:border-collapse prose-code:before:hidden prose-code:after:hidden" >
164+ < div className = "prose prose-sm max-w-none prose-p:my-1 prose-headings:my-2 prose-pre:my-0 prose-pre:bg-transparent prose-pre:p-0 prose-pre:text-ink prose- ul:my-1 prose-ol:my-1 prose-table:my-0 prose-th:px-2 prose-th:py-1 prose-td:px-2 prose-td:py-1 prose-th:border prose-td:border prose-th:border-border prose-td:border-border prose-table:border-collapse prose-code:before:hidden prose-code:after:hidden" >
165165 < Markdown
166166 remarkPlugins = { [ remarkGfm ] }
167167 rehypePlugins = { rehypePlugins as never }
@@ -175,43 +175,53 @@ export function CollapsibleContent({ content, markdown, defaultCollapsed = false
175175 }
176176 return < span { ...props } > { children } </ span > ;
177177 } ,
178- a : ( { node : _node , href, children, ...props } ) => (
179- < a
180- { ...props }
181- href = { href }
182- onClick = { ( e ) => {
183- e . preventDefault ( ) ;
184- if ( ! href ) return ;
185- // Treat anything that isn't a real URL (http/https/mailto/...), a fragment,
186- // or a query as a local filesystem path. Bare relative hrefs like
187- // "output/foo.md" come from markdown links and resolve against the session cwd.
188- const hasScheme = / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 + . - ] * : / . test ( href ) ;
189- const isFragment = href . startsWith ( "#" ) || href . startsWith ( "?" ) ;
190- const isFileUrl = href . startsWith ( "file://" ) ;
191- const isLocalPath = isFileUrl || ( ! hasScheme && ! isFragment ) ;
192-
193- if ( isLocalPath ) {
194- let path = isFileUrl ? href . slice ( 7 ) : href ;
195- try { path = decodeURIComponent ( path ) ; } catch { /* keep raw on bad encoding */ }
196- console . log ( "[link click]" , { href, path, cwd } ) ;
197- invoke ( "open_path" , { path, cwd } ) . catch ( ( err ) => {
198- console . error ( "[open_path failed]" , err ) ;
199- const msg = typeof err === "string" ? err : err instanceof Error ? err . message : JSON . stringify ( err ) ;
200- toast . error ( `打开失败: ${ msg } ` ) ;
201- } ) ;
202- } else {
203- openUrl ( href ) . catch ( ( err ) => {
204- console . error ( "[openUrl failed]" , err ) ;
205- const msg = typeof err === "string" ? err : err instanceof Error ? err . message : JSON . stringify ( err ) ;
206- toast . error ( `打开链接失败: ${ msg } ` ) ;
207- } ) ;
178+ a : ( { node : _node , href, children, ...props } ) => {
179+ // Local path link → route through smart PathLink (existence-checked, context menu).
180+ if ( href ) {
181+ const hasScheme = / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 + . - ] * : / . test ( href ) ;
182+ const isFragment = href . startsWith ( "#" ) || href . startsWith ( "?" ) ;
183+ const isFileUrl = href . startsWith ( "file://" ) ;
184+ if ( isFileUrl || ( ! hasScheme && ! isFragment ) ) {
185+ let key = isFileUrl ? href . slice ( 7 ) : href ;
186+ try { key = decodeURIComponent ( key ) ; } catch { /* keep raw */ }
187+ const hit = pathHits . get ( key ) ;
188+ if ( hit ) {
189+ return < PathLink text = { typeof children === "string" ? children : ( Array . isArray ( children ) ? children . join ( "" ) : key ) } hit = { hit } /> ;
208190 }
209- } }
210- className = "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
211- >
212- { children }
213- </ a >
214- ) ,
191+ }
192+ }
193+ // Fallback: external URL (or unresolved local path) → open via system.
194+ return (
195+ < a
196+ { ...props }
197+ href = { href }
198+ onClick = { ( e ) => {
199+ e . preventDefault ( ) ;
200+ if ( ! href ) return ;
201+ const hasScheme = / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 + . - ] * : / . test ( href ) ;
202+ const isFragment = href . startsWith ( "#" ) || href . startsWith ( "?" ) ;
203+ const isFileUrl = href . startsWith ( "file://" ) ;
204+ const isLocalPath = isFileUrl || ( ! hasScheme && ! isFragment ) ;
205+ if ( isLocalPath ) {
206+ let path = isFileUrl ? href . slice ( 7 ) : href ;
207+ try { path = decodeURIComponent ( path ) ; } catch { /* keep raw */ }
208+ invoke ( "open_path" , { path, cwd } ) . catch ( ( err ) => {
209+ const msg = typeof err === "string" ? err : err instanceof Error ? err . message : JSON . stringify ( err ) ;
210+ toast . error ( `打开失败: ${ msg } ` ) ;
211+ } ) ;
212+ } else {
213+ openUrl ( href ) . catch ( ( err ) => {
214+ const msg = typeof err === "string" ? err : err instanceof Error ? err . message : JSON . stringify ( err ) ;
215+ toast . error ( `打开链接失败: ${ msg } ` ) ;
216+ } ) ;
217+ }
218+ } }
219+ className = "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
220+ >
221+ { children }
222+ </ a >
223+ ) ;
224+ } ,
215225 table : ( { node : _node , ...props } ) => (
216226 < div className = "my-2 overflow-x-auto" >
217227 < table { ...props } />
@@ -221,12 +231,12 @@ export function CollapsibleContent({ content, markdown, defaultCollapsed = false
221231 const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
222232 if ( ! inline && match ) {
223233 return (
224- < div className = "my-2 rounded-md overflow-hidden border border-border " >
234+ < div className = "my-2 rounded-md overflow-hidden" >
225235 < SyntaxHighlighter
226236 style = { warmAcademicTheme }
227237 language = { match [ 1 ] }
228238 PreTag = "div"
229- customStyle = { { margin : 0 , borderRadius : 0 , padding : "0.75rem 1rem" , background : "#F0EEE6" } }
239+ customStyle = { { margin : 0 , borderRadius : "0.375rem" , padding : "0.75rem 1rem" , background : "#F0EEE6" } }
230240 >
231241 { String ( children ) . replace ( / \n $ / , "" ) }
232242 </ SyntaxHighlighter >
0 commit comments