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:
2026-08-19 20:23:55 +00:00
parent 16369f3570
commit 4cd371b55d
@@ -1,3 +1,4 @@
import type { ComponentProps } from 'react';
import { useState, useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -37,7 +38,23 @@ import { injectImages } from './chat-images';
import { randomId } from 'helpers/random-id';
/** 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 = {
...defaultSchema,