/**
* Pure helpers for the new-tab decision + anchor attribute pair used
* by chat-rendered links.
*
* After the navigation unification (ChatCardLoader pre-resolves
* `chatRef.url` via `resolveSourceRowCTA` + `resolveHrefForRuntime`,
* and `ChatCardNavWrap` handles every primary click via `onClickCapture`
* → `handleChatNavClick`), the per-card wrapper only needs
* `{href, target, rel}` on its inner `` so modifier-click /
* hover-preview / copy-link work natively.
*
* `computeIsNewTab` is the SINGLE source of the new-tab rule across
* inline cards, source chips, and `NavLinkAnchorViaRuntime`. Do NOT
* inline the `runtime.navigation.mode === 'embed'` short-circuit + the
* `decideNewTab` fallback chain anywhere else.
*
* Rules:
* - `embed` mode → new-tab, EXCEPT for same-origin absolute hrefs. The chat
* lives on the embedder origin and most content lives on the hub (→ new
* tab), but an embedder may host a few content types in-app (e.g. OpenFrame
* serves releases / roadmap / guides under `/help-center`). Those are
* emitted as ABSOLUTE same-origin URLs by the embedder's `composeContentUrl`;
* keep them same-tab so they soft-nav in-app instead of opening a redundant
* new tab on the same origin. Relative hrefs stay new-tab — in embed mode
* they're hub-relative (absolutized against `defaultContentOrigin`).
* - `host` mode → defer to the runtime's `decideNewTab` callback
* (cross-platform → new-tab) or the lib's default.
*/
import type { ChatRuntime } from '../../../contexts/chat-runtime-context'
import { decideNewTab as libDecideNewTab } from './decide-new-tab'
/** An ABSOLUTE `http(s)` URL on the current page's origin. Relative hrefs return
* false (in embed mode they're hub-relative, not in-app), as do cross-origin
* and non-http URLs. SSR-safe: no `window` → false. */
function isSameOriginAbsoluteHref(href: string): boolean {
if (typeof window === 'undefined') return false
if (!/^https?:\/\//i.test(href)) return false
try {
return new URL(href).origin === window.location.origin
} catch {
return false
}
}
export function computeIsNewTab(
runtime: ChatRuntime,
href: string | null | undefined,
targetPlatform: string | null,
): boolean {
if (!href) return false
if (runtime.navigation.mode === 'embed') return !isSameOriginAbsoluteHref(href)
return (
runtime.navigation.decideNewTab?.({ href, targetPlatform }) ??
libDecideNewTab({
href,
targetPlatform,
currentSource: runtime.source ?? '',
})
)
}
/**
* Returns the `target` + `rel` attribute pair for an inner `` based
* on the new-tab decision. Spread directly into JSX:
*
*
* Single helper so chat anchors stay consistent — new-tab links always
* pair `_blank` with `noopener noreferrer` (defense against window.opener
* tab-nabbing) and same-tab links don't render either attribute.
*/
export function newTabAnchorAttrs(isNewTab: boolean): {
target?: '_blank'
rel?: 'noopener noreferrer'
} {
return isNewTab
? { target: '_blank' as const, rel: 'noopener noreferrer' as const }
: {}
}
/**
* Combine `href` + new-tab attrs into a card's `anchorProps` slot.
* Returns `undefined` (not a falsy object) when there's no URL, so card
* components can branch on `anchorProps != null` to render either a
* clickable `` or a static ``.
*
* Replaces the repeated inline
* `chatRef.url ? { href: chatRef.url, ...newTabAnchorAttrs(isNewTab) } : undefined`
* pattern across the 7 dispatcher wrappers.
*/
export function buildAnchorProps(
href: string | null | undefined,
isNewTab: boolean,
): { href: string; target?: '_blank'; rel?: 'noopener noreferrer' } | undefined {
return href ? { href, ...newTabAnchorAttrs(isNewTab) } : undefined
}