chat: links open in a new tab
officer is a single-page app, so a same-tab navigation to an external url unloads it — tap a link in a message and you come back to a cold boot with the scroll position and anything mid-flight gone. `a` was simply absent from MD_COMPONENTS, so every link rendered as a bare anchor. all four ReactMarkdown call sites in MessageBubble share that object, so one override covers assistant messages, user messages, tool output and the collapsed blocks. noopener noreferrer with it: a bare target="_blank" hands the opened page a live window.opener back into the app. target is set AFTER the prop spread so raw html in a message cannot override it with _self.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ComponentProps } from 'react';
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
@@ -37,7 +38,23 @@ import { injectImages } from './chat-images';
|
|||||||
import { randomId } from 'helpers/random-id';
|
import { randomId } from 'helpers/random-id';
|
||||||
|
|
||||||
/** Shared by the settled bubble and the streaming one, so a block gains nothing when the turn ends. */
|
/** Shared by the settled bubble and the streaming one, so a block gains nothing when the turn ends. */
|
||||||
const MD_COMPONENTS = { pre: CodeBlock, img: ChatImage };
|
/**
|
||||||
|
* Every link in a message opens in a new tab.
|
||||||
|
*
|
||||||
|
* Officer is a single-page app, so a same-tab navigation to an external URL unloads it: the user taps a
|
||||||
|
* link in a chat and comes back to a cold boot, having lost the scroll position and anything mid-flight.
|
||||||
|
* A message is a place someone reads FROM, not a place they navigate away from.
|
||||||
|
*
|
||||||
|
* `noopener noreferrer` is not decoration — a bare `target="_blank"` hands the opened page a live
|
||||||
|
* `window.opener` back into the app, which is a redirect away from anything an agent linked to.
|
||||||
|
*/
|
||||||
|
const ChatLink = ({ href, children, ...rest }: ComponentProps<'a'>) => (
|
||||||
|
<a {...rest} href={href} target="_blank" rel="noopener noreferrer">
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MD_COMPONENTS = { pre: CodeBlock, img: ChatImage, a: ChatLink };
|
||||||
|
|
||||||
const sanitizeSchema = {
|
const sanitizeSchema = {
|
||||||
...defaultSchema,
|
...defaultSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user