Compare commits

..

1 Commits

Author SHA1 Message Date
Nolann B. 2c9e8580dd Answer Assistant questions with medium reasoning 2026-10-20 17:42:19 +02:00
41 changed files with 62 additions and 290 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Answer Assistant questions with medium reasoning instead of low, matching AI search.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Mark blocks, pages and titles so comments can target them on change request previews.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix the double slash in the example page URLs of the MCP server's getPage and sendFeedback tools.
@@ -62,7 +62,7 @@ export async function* streamAIChatResponse({
}, },
] ]
: [], : [],
model: AIModel.ReasoningLow, model: AIModel.ReasoningMedium,
previousResponseId, previousResponseId,
toolCall, toolCall,
tools, tools,
@@ -3,7 +3,6 @@ import type { DocumentInlineAnnotation } from '@gitbook/api';
import { Blocks } from '../Blocks'; import { Blocks } from '../Blocks';
import type { InlineProps } from '../Inline'; import type { InlineProps } from '../Inline';
import { Inlines } from '../Inlines'; import { Inlines } from '../Inlines';
import { getCommentMarkerProps } from '../utils';
import { AnnotationPopover } from './AnnotationPopover'; import { AnnotationPopover } from './AnnotationPopover';
import { getNodeFragmentByType } from '@/lib/document'; import { getNodeFragmentByType } from '@/lib/document';
@@ -22,19 +21,13 @@ export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
return ( return (
<AnnotationPopover <AnnotationPopover
body={ body={
// The app doesn't allow comments inside annotation bodies. <Blocks
<div document={document}
{...getCommentMarkerProps(context, 'data-comment-ignore')} ancestorBlocks={[]}
className="contents" context={context}
> nodes={fragment.nodes}
<Blocks style={['contents']}
document={document} />
ancestorBlocks={[]}
context={context}
nodes={fragment.nodes}
style={['contents']}
/>
</div>
} }
> >
{content} {content}
@@ -6,7 +6,6 @@ import {
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard'; import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
import { getCommentAnchorProps } from './utils';
import { Card, type CardProps } from '@/components/primitives'; import { Card, type CardProps } from '@/components/primitives';
import { import {
type ResolvedContentRef, type ResolvedContentRef,
@@ -32,7 +31,6 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
return ( return (
<NotFoundRefHoverCard context={context}> <NotFoundRefHoverCard context={context}>
<BlockContentRefCard <BlockContentRefCard
{...getCommentAnchorProps(context, block.key)}
contentRef={block.data.ref} contentRef={block.data.ref}
resolved={fallback} resolved={fallback}
style={style} style={style}
@@ -41,14 +39,7 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
); );
} }
return ( return <BlockContentRefCard contentRef={block.data.ref} resolved={resolved} style={style} />;
<BlockContentRefCard
{...getCommentAnchorProps(context, block.key)}
contentRef={block.data.ref}
resolved={resolved}
style={style}
/>
);
} }
function BlockContentRefCard( function BlockContentRefCard(
@@ -9,7 +9,6 @@ import type {
import type { DocumentContextProps } from './DocumentView'; import type { DocumentContextProps } from './DocumentView';
import { Inlines } from './Inlines'; import { Inlines } from './Inlines';
import { getCommentAnchorProps } from './utils';
import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document'; import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document';
import { type ClassValue, tcls } from '@/lib/tailwind'; import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -35,11 +34,6 @@ export function Caption(
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile; block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
withBorder?: boolean; withBorder?: boolean;
withFrame?: boolean; withFrame?: boolean;
/**
* Expose the block key for commenting; off inside an `images` block, which carries the key
* itself since its images aren't comment targets.
*/
withBlockKey?: boolean;
} & DocumentContextProps } & DocumentContextProps
) { ) {
const { const {
@@ -65,11 +59,8 @@ export function Caption(
], ],
style, style,
align = 'center', align = 'center',
withBlockKey = false,
} = props; } = props;
const anchorProps = withBlockKey ? getCommentAnchorProps(context, block.key) : {};
const caption = getNodeFragmentByName(block, 'caption'); const caption = getNodeFragmentByName(block, 'caption');
const captionParagraph = caption?.nodes[0]; const captionParagraph = caption?.nodes[0];
@@ -78,15 +69,11 @@ export function Caption(
captionParagraph.type !== 'paragraph' || captionParagraph.type !== 'paragraph' ||
isNodeEmpty(captionParagraph) isNodeEmpty(captionParagraph)
) { ) {
return ( return <div className={tcls(style, wrapperStyle)}>{children}</div>;
<div {...anchorProps} className={tcls(style, wrapperStyle)}>
{children}
</div>
);
} }
return ( return (
<picture {...anchorProps} className={tcls('relative', style)}> <picture className={tcls('relative', style)}>
<div className={tcls(wrapperStyle, captionAlignStyles[align])}>{children}</div> <div className={tcls(wrapperStyle, captionAlignStyles[align])}>{children}</div>
<figcaption <figcaption
className={tcls( className={tcls(
@@ -23,8 +23,6 @@ export type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'st
mode: BlockProps<DocumentBlockCode>['context']['mode']; mode: BlockProps<DocumentBlockCode>['context']['mode'];
themes?: CustomizationThemedCodeTheme; themes?: CustomizationThemedCodeTheme;
embedded?: boolean; embedded?: boolean;
/** Node key for comments, set only where commenting is possible. */
commentKey?: string;
}; };
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10; export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
@@ -34,8 +32,7 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting. * It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
*/ */
export function ClientCodeBlock(props: ClientBlockProps) { export function ClientCodeBlock(props: ClientBlockProps) {
const { block, mode, style, inlines, inlineExprVariables, themes, embedded, commentKey } = const { block, mode, style, inlines, inlineExprVariables, themes, embedded } = props;
props;
const blockRef = useRef<HTMLDivElement>(null); const blockRef = useRef<HTMLDivElement>(null);
const isInViewportRef = useRef(false); const isInViewportRef = useRef(false);
const [isInViewport, setIsInViewport] = useState(false); const [isInViewport, setIsInViewport] = useState(false);
@@ -148,7 +145,6 @@ export function ClientCodeBlock(props: ClientBlockProps) {
id={codeBlockBodyId} id={codeBlockBodyId}
isPrint={mode === 'print'} isPrint={mode === 'print'}
embedded={embedded} embedded={embedded}
commentKey={commentKey}
/> />
); );
@@ -8,7 +8,6 @@ import type {
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks'; import { Blocks } from '../Blocks';
import { getCommentKeyProp, getCommentMarkerProps } from '../utils';
import { ClientCodeBlock } from './ClientCodeBlock'; import { ClientCodeBlock } from './ClientCodeBlock';
import { CodeBlockRenderer } from './CodeBlockRenderer'; import { CodeBlockRenderer } from './CodeBlockRenderer';
import { highlight } from './highlight'; import { highlight } from './highlight';
@@ -60,20 +59,14 @@ export async function CodeBlock(
return null; return null;
} }
return ( return (
// The app doesn't allow comments inside annotation bodies. <Blocks
<div
key={index} key={index}
{...getCommentMarkerProps(context, 'data-comment-ignore')} document={document}
className="contents" ancestorBlocks={[]}
> context={context}
<Blocks nodes={fragment.nodes}
document={document} style="space-y-4"
ancestorBlocks={[]} />
context={context}
nodes={fragment.nodes}
style="space-y-4"
/>
</div>
); );
})(); })();
@@ -99,7 +92,6 @@ export async function CodeBlock(
theme={theme} theme={theme}
isPrint={context.mode === 'print'} isPrint={context.mode === 'print'}
embedded={embedded} embedded={embedded}
{...getCommentKeyProp(context, block.key)}
/> />
); );
} }
@@ -122,7 +114,6 @@ export async function CodeBlock(
mode: context.mode, mode: context.mode,
themes, themes,
embedded, embedded,
...getCommentKeyProp(context, block.key),
}; };
return ( return (
@@ -5,7 +5,6 @@ import type { DocumentBlockCode } from '@gitbook/api';
import { AnnotationPopover } from '../Annotation/AnnotationPopover'; import { AnnotationPopover } from '../Annotation/AnnotationPopover';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentKeyAttributes } from '../utils/commentAnchors';
import { AskAICodeButton } from './AskAICodeButton'; import { AskAICodeButton } from './AskAICodeButton';
import { CopyCodeButton } from './CopyCodeButton'; import { CopyCodeButton } from './CopyCodeButton';
import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight-tokens'; import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight-tokens';
@@ -13,8 +12,6 @@ import { tcls } from '@/lib/tailwind';
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & { type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
theme: HighlightTheme; theme: HighlightTheme;
/** Node key for comments, set only where commenting is possible. */
commentKey?: string;
'aria-busy'?: boolean; 'aria-busy'?: boolean;
id?: string; id?: string;
/** /**
@@ -32,7 +29,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
props: CodeBlockRendererProps, props: CodeBlockRendererProps,
ref: React.ForwardedRef<HTMLDivElement> ref: React.ForwardedRef<HTMLDivElement>
) { ) {
const { block, style, theme, 'aria-busy': ariaBusy, isPrint, embedded, commentKey } = props; const { block, style, theme, 'aria-busy': ariaBusy, isPrint, embedded } = props;
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1; const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
const withWrap = block.data.overflow === 'wrap'; const withWrap = block.data.overflow === 'wrap';
@@ -53,7 +50,6 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
<div <div
ref={ref} ref={ref}
id={codeId} id={codeId}
{...getCommentKeyAttributes(commentKey)}
aria-busy={ariaBusy} aria-busy={ariaBusy}
className={tcls( className={tcls(
'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible', 'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible',
@@ -218,12 +218,7 @@ export function MermaidCodeBlock(
return ( return (
<> <>
{/* Inline slot: hosts the diagram in the document flow until it goes fullscreen. */} {/* Inline slot: hosts the diagram in the document flow until it goes fullscreen. */}
<div <div ref={rootRef} className={tcls('relative', style)} contentEditable={false} />
ref={rootRef}
data-block-key={props.commentKey}
className={tcls('relative', style)}
contentEditable={false}
/>
{diagramHostRef.current ? createPortal(diagram, diagramHostRef.current) : null} {diagramHostRef.current ? createPortal(diagram, diagramHostRef.current) : null}
<Dialog.Root <Dialog.Root
open={isFullscreen} open={isFullscreen}
@@ -4,7 +4,6 @@ import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitb
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks'; import { Blocks } from '../Blocks';
import { getCommentMarkerProps } from '../utils';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
export function Columns(props: BlockProps<DocumentBlockColumns>) { export function Columns(props: BlockProps<DocumentBlockColumns>) {
@@ -38,8 +37,6 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
return ( return (
<div <div
// The app doesn't allow comments inside columns.
{...getCommentMarkerProps(context, 'data-comment-ignore')}
className={tcls( className={tcls(
'grid w-full @2xl:grid-flow-col @2xl:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] grid-cols-1 @2xl:grid-rows-1 gap-x-8 gap-y-4', 'grid w-full @2xl:grid-flow-col @2xl:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] grid-cols-1 @2xl:grid-rows-1 gap-x-8 gap-y-4',
style style
@@ -37,12 +37,6 @@ export interface DocumentContext {
*/ */
withLinkPreviews?: boolean; withLinkPreviews?: boolean;
/**
* Render the markers comments anchor to (block keys, table cells). Only where commenting is
* possible, so public pages don't carry them.
*/
withCommentAnchors?: boolean;
/** /**
* True when this document is the main page body — the only place a background page cover is * True when this document is the main page body — the only place a background page cover is
* shown behind the content. Cover-aware text styling (contrast over the cover) is scoped to it * shown behind the content. Cover-aware text styling (contrast over the cover) is scoped to it
@@ -18,7 +18,7 @@ export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
} }
return ( return (
<Caption {...props} withBlockKey> <Caption {...props}>
<Image <Image
sources={{ sources={{
light: { light: {
@@ -32,7 +32,7 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
} }
return ( return (
<Caption {...props} withBorder withBlockKey> <Caption {...props} withBorder>
{embed.type === 'rich' ? ( {embed.type === 'rich' ? (
<> <>
<div <div
@@ -14,8 +14,6 @@ export function Details(props: {
contentIds?: string[]; contentIds?: string[];
open?: boolean; open?: boolean;
className?: ClassValue; className?: ClassValue;
/** Node key of the expandable, so comments can anchor to it. */
commentKey?: string;
}) { }) {
const { children, id, className } = props; const { children, id, className } = props;
@@ -48,7 +46,6 @@ export function Details(props: {
<details <details
ref={ref} ref={ref}
id={id} id={id}
data-block-key={props.commentKey}
open={props.open || openFromHash} open={props.open || openFromHash}
className={tcls( className={tcls(
className, className,
@@ -5,7 +5,6 @@ import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks'; import { Blocks } from '../Blocks';
import { shouldShowHashLinks } from '../HashLinkButton'; import { shouldShowHashLinks } from '../HashLinkButton';
import { Inlines } from '../Inlines'; import { Inlines } from '../Inlines';
import { getCommentAnchorProps, getCommentKeyProp } from '../utils';
import { Details } from './Details'; import { Details } from './Details';
import { ToggleChevron } from '@/components/primitives'; import { ToggleChevron } from '@/components/primitives';
import { getSpaceLanguage, tString } from '@/intl/server'; import { getSpaceLanguage, tString } from '@/intl/server';
@@ -38,12 +37,10 @@ export async function Expandable(props: BlockProps<DocumentBlockExpandable>) {
return ( return (
<Details <Details
id={id} id={id}
{...getCommentKeyProp(context, block.key)}
open={context.mode === 'print' || isDefaultExpanded} open={context.mode === 'print' || isDefaultExpanded}
className={tcls('scroll-mt-(--content-scroll-margin)', style)} className={tcls('scroll-mt-(--content-scroll-margin)', style)}
> >
<summary <summary
{...getCommentAnchorProps(context, titleParagraph.key)}
className={tcls( className={tcls(
'cursor-pointer', 'cursor-pointer',
'px-4', 'px-4',
@@ -40,7 +40,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
}; };
return ( return (
<Caption {...props} withBorder withBlockKey> <Caption {...props} withBorder>
<div className="flex flex-wrap items-center gap-5 px-5 py-3"> <div className="flex flex-wrap items-center gap-5 px-5 py-3">
<div className="flex min-h-8 min-w-14 flex-col items-center justify-center gap-1 border-r border-tint-subtle pr-4"> <div className="flex min-h-8 min-w-14 flex-col items-center justify-center gap-1 border-r border-tint-subtle pr-4">
{contentType === 'image' ? ( {contentType === 'image' ? (
@@ -5,7 +5,7 @@ import { HashLinkButton, hashLinkButtonWrapperStyles, shouldShowHashLinks } from
import { HeadingRevealWrapper } from './HeadingRevealWrapper'; import { HeadingRevealWrapper } from './HeadingRevealWrapper';
import { Inlines } from './Inlines'; import { Inlines } from './Inlines';
import { getBlockTextStyle } from './spacing'; import { getBlockTextStyle } from './spacing';
import { getCommentAnchorProps, getTextAlignment } from './utils'; import { getTextAlignment } from './utils';
import { getSpaceLanguage, tString } from '@/intl/server'; import { getSpaceLanguage, tString } from '@/intl/server';
import { defaultLanguage } from '@/intl/translations'; import { defaultLanguage } from '@/intl/translations';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
@@ -30,7 +30,6 @@ export async function Heading(props: BlockProps<DocumentBlockHeading>) {
<HeadingRevealWrapper <HeadingRevealWrapper
as={Tag} as={Tag}
id={id} id={id}
{...getCommentAnchorProps(context, block.key)}
className={tcls( className={tcls(
textStyle.textSize, textStyle.textSize,
'heading', 'heading',
@@ -3,7 +3,6 @@ import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } fr
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { Caption, type CaptionAlign } from './Caption'; import { Caption, type CaptionAlign } from './Caption';
import type { DocumentContext } from './DocumentView'; import type { DocumentContext } from './DocumentView';
import { getCommentAnchorProps } from './utils';
import { Image, type ImageResponsiveSize } from '@/components/utils'; import { Image, type ImageResponsiveSize } from '@/components/utils';
import { resolveContentRefInDocument } from '@/lib/references'; import { resolveContentRefInDocument } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind'; import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -16,7 +15,6 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
return ( return (
<div <div
{...getCommentAnchorProps(context, block.key)}
className={tcls( className={tcls(
style, style,
'flex', 'flex',
@@ -2,10 +2,9 @@ import type { DocumentBlockIntegration, RenderIntegrationUI } from '@gitbook/api
import { ContentKitOutput } from '@gitbook/react-contentkit'; import { ContentKitOutput } from '@gitbook/react-contentkit';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
import { getWebframePageContext } from './adaptive'; import { getWebframePageContext } from './adaptive';
import './contentkit.css';
import { contentKitServerContext } from './contentkit'; import { contentKitServerContext } from './contentkit';
import './contentkit.css';
import { import {
ContentKitWithClientContext, ContentKitWithClientContext,
type WebframeLinkerData, type WebframeLinkerData,
@@ -62,12 +61,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
} }
return ( return (
<div <div className={tcls(style)}>
// Integration output can render blocks with generated keys.
{...getCommentAnchorProps(context, block.key)}
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls(style)}
>
<pre> <pre>
Unexpected error with integration {block.data.integration}:{' '} Unexpected error with integration {block.data.integration}:{' '}
{initialResponse.error.message} {initialResponse.error.message}
@@ -105,12 +99,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
}; };
return ( return (
<div <div className={tcls(style)}>
// Integration output can render blocks with generated keys.
{...getCommentAnchorProps(context, block.key)}
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls(style)}
>
<ContentKitWithClientContext <ContentKitWithClientContext
{...contentKitProps} {...contentKitProps}
canAccessVisitorClaims={canAccessVisitorClaims} canAccessVisitorClaims={canAccessVisitorClaims}
@@ -11,13 +11,11 @@ import type {
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { Blocks } from './Blocks'; import { Blocks } from './Blocks';
import { getBlockTextStyle } from './spacing'; import { getBlockTextStyle } from './spacing';
import { getCommentAnchorProps } from './utils';
import { Checkbox } from '@/components/primitives'; import { Checkbox } from '@/components/primitives';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
export function ListItem(props: BlockProps<DocumentBlockListItem>) { export function ListItem(props: BlockProps<DocumentBlockListItem>) {
const { block, ancestorBlocks, ...contextProps } = props; const { block, ancestorBlocks, ...contextProps } = props;
const { context } = contextProps;
const parent = ancestorBlocks[ancestorBlocks.length - 1]; const parent = ancestorBlocks[ancestorBlocks.length - 1];
assert( assert(
@@ -48,7 +46,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
switch (parent.type) { switch (parent.type) {
case 'list-tasks': case 'list-tasks':
return ( return (
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}> <ListItemLI block={block}>
<ListItemPrefix block={block}> <ListItemPrefix block={block}>
<Checkbox <Checkbox
id={block.key!} id={block.key!}
@@ -66,7 +64,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
); );
case 'list-ordered': case 'list-ordered':
return ( return (
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}> <ListItemLI block={block}>
<ListItemPrefix block={block}> <ListItemPrefix block={block}>
<PseudoBefore <PseudoBefore
content={getOrderedListItemPrefixContent({ content={getOrderedListItemPrefixContent({
@@ -84,7 +82,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
); );
case 'list-unordered': case 'list-unordered':
return ( return (
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}> <ListItemLI block={block}>
<ListItemPrefix block={block}> <ListItemPrefix block={block}>
<PseudoBefore <PseudoBefore
content={getUnorderedListItemsPrefixContent({ content={getUnorderedListItemsPrefixContent({
@@ -125,17 +123,9 @@ function getListItemDepth(input: {
return depth; return depth;
} }
function ListItemLI(props: { function ListItemLI(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
block: DocumentBlockListItem;
anchorProps: ReturnType<typeof getCommentAnchorProps>;
children: React.ReactNode;
}) {
const textStyle = getBlockTextStyle(props.block); const textStyle = getBlockTextStyle(props.block);
return ( return <li className={tcls(textStyle.lineHeight, 'flex items-start')}>{props.children}</li>;
<li {...props.anchorProps} className={tcls(textStyle.lineHeight, 'flex items-start')}>
{props.children}
</li>
);
} }
function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) { function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
@@ -3,25 +3,21 @@ import { MathFormula } from '@gitbook/react-math';
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import type { InlineProps } from './Inline'; import type { InlineProps } from './Inline';
import { getCommentAnchorProps } from './utils';
import { getAssetURL } from '@/lib/assets'; import { getAssetURL } from '@/lib/assets';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
const assetsUrl = getAssetURL('math'); const assetsUrl = getAssetURL('math');
export async function BlockMath(props: BlockProps<DocumentBlockMath>) { export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
const { block, style, context } = props; const { block, style } = props;
// `MathFormula` only forwards `className`, so the comment anchor needs its own wrapper.
return ( return (
<div {...getCommentAnchorProps(context, block.key)} className={tcls(style)}> <MathFormula
<MathFormula formula={block.data.formula}
formula={block.data.formula} inline={false}
inline={false} className={tcls(style, 'overflow-x-auto')}
className="overflow-x-auto" assetsUrl={assetsUrl}
assetsUrl={assetsUrl} />
/>
</div>
); );
} }
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
import { getOpenAPIBlockClientProps } from './context'; import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy'; import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock'; import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi block or an openapi-operation block. * Render an openapi block or an openapi-operation block.
*/ */
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) { export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
const { style, block, context } = props; const { style } = props;
return ( return (
<div <div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
{...getCommentAnchorProps(context, block.key)}
// Its generated headings and descriptions carry keys that aren't page nodes.
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
>
<OpenAPIOperationBody {...props} /> <OpenAPIOperationBody {...props} />
</div> </div>
); );
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
import { getOpenAPIBlockClientProps } from './context'; import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy'; import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock'; import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi-schemas block. * Render an openapi-schemas block.
*/ */
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) { export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
const { style, block, context } = props; const { style } = props;
return ( return (
<div <div className={tcls('flex w-full', style, 'max-w-full')}>
{...getCommentAnchorProps(context, block.key)}
// Its generated headings and descriptions carry keys that aren't page nodes.
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls('flex w-full', style, 'max-w-full')}
>
<OpenAPISchemasBody {...props} /> <OpenAPISchemasBody {...props} />
</div> </div>
); );
@@ -1,5 +1,4 @@
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
import { getOpenAPIBlockClientProps } from './context'; import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy'; import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock'; import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi block or an openapi-webhook block. * Render an openapi block or an openapi-webhook block.
*/ */
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) { export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
const { style, block, context } = props; const { style } = props;
return ( return (
<div <div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
{...getCommentAnchorProps(context, block.key)}
// Its generated headings and descriptions carry keys that aren't page nodes.
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
>
<OpenAPIWebhookBody {...props} /> <OpenAPIWebhookBody {...props} />
</div> </div>
); );
@@ -4,19 +4,15 @@ import { CustomizationAIMode } from '@gitbook/api';
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { Inlines } from './Inlines'; import { Inlines } from './Inlines';
import { getTextAlignment } from './utils'; import { getTextAlignment } from './utils';
import { getCommentAnchorProps } from './utils';
import { getNodeText } from '@/lib/document'; import { getNodeText } from '@/lib/document';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) { export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
const { block, style, ancestorBlocks, ...contextProps } = props; const { block, style, ancestorBlocks, ...contextProps } = props;
const { context } = contextProps; const { context } = contextProps;
// Like the app, a list item owns the comments on its direct paragraph.
const isCommentTarget = ancestorBlocks.at(-1)?.type !== 'list-item';
const paragraph = ( const paragraph = (
<p <p
{...(isCommentTarget ? getCommentAnchorProps(context, block.key) : {})}
// Cover-aware contrast text applies only to the page body, not to documents // Cover-aware contrast text applies only to the page body, not to documents
// rendered in overlays (search answers, AI chat) on a background-cover page. // rendered in overlays (search answers, AI chat) on a background-cover page.
data-cover-aware-text={context.isPageBody ? '' : undefined} data-cover-aware-text={context.isPageBody ? '' : undefined}
@@ -9,14 +9,12 @@ import { validateIconName } from '@gitbook/icons/icons';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { CodeBlock } from '../CodeBlock'; import { CodeBlock } from '../CodeBlock';
import { getPlainCodeBlock } from '../CodeBlock/highlight-tokens'; import { getPlainCodeBlock } from '../CodeBlock/highlight-tokens';
import { COMMENT_IGNORE_CLASSNAME, getCommentKeyProp } from '../utils';
import { PromptClient } from './PromptClient'; import { PromptClient } from './PromptClient';
import { tcls } from '@/lib/tailwind';
const PROMPT_COLLAPSED_LINE_COUNT = 10; const PROMPT_COLLAPSED_LINE_COUNT = 10;
export function Prompt(props: BlockProps<DocumentBlockPrompt>) { export function Prompt(props: BlockProps<DocumentBlockPrompt>) {
const { block, document, ancestorBlocks, context, isEstimatedOffscreen, style } = props; const { block, document, ancestorBlocks, context, isEstimatedOffscreen } = props;
const contentIcon = const contentIcon =
block.data.icon && validateIconName(block.data.icon) ? block.data.icon : null; block.data.icon && validateIconName(block.data.icon) ? block.data.icon : null;
const prompt = getPromptText(block); const prompt = getPromptText(block);
@@ -41,13 +39,6 @@ export function Prompt(props: BlockProps<DocumentBlockPrompt>) {
defaultExpanded={defaultExpanded} defaultExpanded={defaultExpanded}
openInAIProviders={getOpenInAIProviders(props)} openInAIProviders={getOpenInAIProviders(props)}
prompt={prompt} prompt={prompt}
{...getCommentKeyProp(context, block.key)}
// Only the comment marker: the prompt keeps its own width rather than the block styles.
className={
tcls(style).split(' ').includes(COMMENT_IGNORE_CLASSNAME)
? COMMENT_IGNORE_CLASSNAME
: undefined
}
> >
{promptContent} {promptContent}
</PromptClient> </PromptClient>
@@ -13,9 +13,6 @@ import { tcls } from '@/lib/tailwind';
type PromptClientProps = DocumentBlockPrompt['data'] & { type PromptClientProps = DocumentBlockPrompt['data'] & {
contentIcon: IconName | null; contentIcon: IconName | null;
prompt: string; prompt: string;
/** Comment target: its code nodes aren't, so the prompt owns clicks inside it. */
commentKey?: string;
className?: string;
children?: React.ReactNode; children?: React.ReactNode;
}; };
@@ -26,8 +23,6 @@ export function PromptClient(props: PromptClientProps) {
openInAIProviders = false, openInAIProviders = false,
defaultExpanded = 'hidden', defaultExpanded = 'hidden',
prompt, prompt,
commentKey,
className,
children, children,
} = props; } = props;
const language = useLanguage(); const language = useLanguage();
@@ -42,14 +37,11 @@ export function PromptClient(props: PromptClientProps) {
return ( return (
<div <div
data-block-key={commentKey}
data-comment-block={commentKey ? '' : undefined}
className={tcls( className={tcls(
'relative flex w-full flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs text-tint-strong', 'relative flex w-full flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs text-tint-strong',
'border border-tint-subtle contrast-more:border-tint', 'border border-tint-subtle contrast-more:border-tint',
'transition', 'transition',
expanded ? 'bg-tint depth-subtle:shadow-xs' : 'bg-tint-base', expanded ? 'bg-tint depth-subtle:shadow-xs' : 'bg-tint-base'
className
)} )}
> >
<div <div
@@ -2,7 +2,6 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { UnwrappedBlocks } from './Blocks'; import { UnwrappedBlocks } from './Blocks';
import { COMMENT_IGNORE_CLASSNAME } from './utils';
import { getDataOrNull } from '@/lib/data'; import { getDataOrNull } from '@/lib/data';
import { resolveContentRefInDocument } from '@/lib/references'; import { resolveContentRefInDocument } from '@/lib/references';
@@ -51,9 +50,6 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
nodes={reusableContentDocument.nodes} nodes={reusableContentDocument.nodes}
document={reusableContentDocument} document={reusableContentDocument}
ancestorBlocks={[...ancestorBlocks, block]} ancestorBlocks={[...ancestorBlocks, block]}
// Its blocks carry keys of the reusable content's own document, which comments on the
// page can't anchor to. A class, not a wrapper, which would break the page's spacing.
blockStyle={context.withCommentAnchors ? COMMENT_IGNORE_CLASSNAME : undefined}
context={{ context={{
...context, ...context,
contentContext: reusableContent.context, contentContext: reusableContent.context,
@@ -4,12 +4,10 @@ import type { DocumentBlockStepperStep } from '@gitbook/api';
import type { BlockProps } from './Block'; import type { BlockProps } from './Block';
import { Blocks } from './Blocks'; import { Blocks } from './Blocks';
import { getCommentAnchorProps } from './utils';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) { export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
const { block, style, ancestorBlocks, ...contextProps } = props; const { block, style, ancestorBlocks, ...contextProps } = props;
const { context } = contextProps;
const ancestor = ancestorBlocks[ancestorBlocks.length - 1]; const ancestor = ancestorBlocks[ancestorBlocks.length - 1];
assert(ancestor?.type === 'stepper', 'Ancestor block must be a stepper'); assert(ancestor?.type === 'stepper', 'Ancestor block must be a stepper');
@@ -34,10 +32,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
})(); })();
return ( return (
<div <div className={tcls('flex flex-row gap-4 md:gap-8', style)}>
{...getCommentAnchorProps(context, block.key)}
className={tcls('flex flex-row gap-4 md:gap-8', style)}
>
<div className="relative select-none"> <div className="relative select-none">
<div <div
className={tcls( className={tcls(
@@ -1,4 +1,3 @@
import { getCommentCellProps } from '../utils';
import { getTableCellMerge } from './cellMerges'; import { getTableCellMerge } from './cellMerges';
import { getColumnWidth, getViewGridLayout, hasVisibleHeader } from './layout'; import { getColumnWidth, getViewGridLayout, hasVisibleHeader } from './layout';
import { RecordColumnValue } from './RecordColumnValue'; import { RecordColumnValue } from './RecordColumnValue';
@@ -6,7 +5,7 @@ import { StickyViewGrid } from './StickyViewGrid';
import type { TableGridViewProps } from './Table'; import type { TableGridViewProps } from './Table';
import { TableHoverTable } from './TableHoverTable'; import { TableHoverTable } from './TableHoverTable';
import { TableSearchTableBody } from './TableSearch'; import { TableSearchTableBody } from './TableSearch';
import { getColumnAlignment, getColumnVerticalAlignment, type VerticalAlignment } from './utils'; import { type VerticalAlignment, getColumnAlignment, getColumnVerticalAlignment } from './utils';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
/** Semantic table renderer used when a cell spans multiple records. */ /** Semantic table renderer used when a cell spans multiple records. */
@@ -142,7 +141,7 @@ function NativeViewGridBody(
stickyFirstColumn: boolean; stickyFirstColumn: boolean;
} }
) { ) {
const { block, view, records, withHeader, stickyFirstColumn, cellMergeLayout, context } = props; const { block, view, records, withHeader, stickyFirstColumn, cellMergeLayout } = props;
const firstVisibleColumn = view.columns[0]; const firstVisibleColumn = view.columns[0];
const lastVisibleColumn = view.columns.at(-1); const lastVisibleColumn = view.columns.at(-1);
const recordsById = new Map(records.map((record) => [record[0], record] as const)); const recordsById = new Map(records.map((record) => [record[0], record] as const));
@@ -220,7 +219,6 @@ function NativeViewGridBody(
colSpan={colSpan} colSpan={colSpan}
aria-rowspan={rowSpan} aria-rowspan={rowSpan}
aria-colspan={colSpan} aria-colspan={colSpan}
{...getCommentCellProps(context, recordId, column)}
data-table-row-start={recordIndex} data-table-row-start={recordIndex}
data-table-row-end={ data-table-row-end={
recordIndex + (cellMerge?.merge.rowSpan ?? 1) - 1 recordIndex + (cellMerge?.merge.rowSpan ?? 1) - 1
@@ -1,4 +1,3 @@
import { getCommentCellProps } from '../utils';
import { getMergedCellWidth, getTableCellMerge } from './cellMerges'; import { getMergedCellWidth, getTableCellMerge } from './cellMerges';
import { getColumnWidth } from './layout'; import { getColumnWidth } from './layout';
import { RecordColumnValue } from './RecordColumnValue'; import { RecordColumnValue } from './RecordColumnValue';
@@ -55,7 +54,6 @@ export function RecordRow(
<div <div
key={column} key={column}
role="cell" role="cell"
{...getCommentCellProps(context, record[0], column)}
aria-colspan={cellMerge?.merge.colSpan} aria-colspan={cellMerge?.merge.colSpan}
className={tcls( className={tcls(
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0', 'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
@@ -3,7 +3,7 @@ import assertNever from 'assert-never';
import type { DocumentBlockTable, DocumentTableViewGrid } from '@gitbook/api'; import type { DocumentBlockTable, DocumentTableViewGrid } from '@gitbook/api';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { getCommentAnchorProps, getCommentMarkerProps, isBlockOffscreen } from '../utils'; import { isBlockOffscreen } from '../utils';
import { type TableCellMergeLayout, createTableCellMergeLayout } from './cellMerges'; import { type TableCellMergeLayout, createTableCellMergeLayout } from './cellMerges';
import { getViewGridLayout, hasVisibleHeader } from './layout'; import { getViewGridLayout, hasVisibleHeader } from './layout';
import { NativeViewGrid } from './NativeViewGrid'; import { NativeViewGrid } from './NativeViewGrid';
@@ -74,12 +74,7 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
block.data.view.type === 'grid' ? cellMergeLayout.recordGroups : undefined block.data.view.type === 'grid' ? cellMergeLayout.recordGroups : undefined
} }
> >
<div <div className={tcls(style, 'flex flex-col gap-3')}>
{...getCommentAnchorProps(context, block.key)}
// Owns clicks inside, e.g. on blocks of a record's own document; cells still win.
{...getCommentMarkerProps(context, 'data-comment-block')}
className={tcls(style, 'flex flex-col gap-3')}
>
{showSearch ? ( {showSearch ? (
<TableSearchInput <TableSearchInput
selectColumns={selectColumns} selectColumns={selectColumns}
@@ -1,49 +0,0 @@
import type { DocumentContext } from '../DocumentView';
// Spread rather than set to `undefined`: an `undefined` prop is still written to the RSC payload,
// so public pages would keep paying for the attribute names.
/** Marks blocks comments can't anchor to, passed down as their `style` (see `ReusableContent`). */
export const COMMENT_IGNORE_CLASSNAME = 'comment-ignore';
/** The node key comments anchor to, only where commenting is possible. */
export function getCommentAnchorProps(
context: DocumentContext,
key: string | undefined
): { 'data-block-key'?: string } {
return context.withCommentAnchors && key ? { 'data-block-key': key } : {};
}
/** A table cell's record and column, only where commenting is possible. */
export function getCommentCellProps(
context: DocumentContext,
record: string,
column: string
): { 'data-table-record'?: string; 'data-table-column'?: string } {
return context.withCommentAnchors
? { 'data-table-record': record, 'data-table-column': column }
: {};
}
/** A boolean comment marker, only where commenting is possible. */
export function getCommentMarkerProps(
context: DocumentContext,
name: 'data-comment-block' | 'data-comment-ignore'
): Partial<Record<typeof name, ''>> {
return context.withCommentAnchors ? { [name]: '' } : {};
}
/** The node key as a `commentKey` prop for components rendering it themselves (e.g. client ones). */
export function getCommentKeyProp(
context: DocumentContext,
key: string | undefined
): { commentKey?: string } {
return context.withCommentAnchors && key ? { commentKey: key } : {};
}
/** The anchor attribute for a `commentKey` prop, when set. */
export function getCommentKeyAttributes(commentKey: string | undefined): {
'data-block-key'?: string;
} {
return commentKey ? { 'data-block-key': commentKey } : {};
}
@@ -1,3 +1,2 @@
export * from './isBlockOffscreen'; export * from './isBlockOffscreen';
export * from './textAlignment'; export * from './textAlignment';
export * from './commentAnchors';
@@ -58,9 +58,6 @@ export async function PageBody(props: {
? hasTopLevelBlock(document, (block) => block.type === 'updates') ? hasTopLevelBlock(document, (block) => block.type === 'updates')
: false; : false;
// Comments can only be made on change request previews, so public pages skip their markers.
const withCommentAnchors = Boolean(context.changeRequest);
// Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document. // Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
const withLinkPreviews = document const withLinkPreviews = document
? !hasMoreThan( ? !hasMoreThan(
@@ -103,8 +100,6 @@ export async function PageBody(props: {
/> />
) : null} ) : null}
<main <main
// Comments on the whole page target anywhere in it that isn't a block.
{...(withCommentAnchors ? { 'data-comment-page': '' } : {})}
className={tcls( className={tcls(
'relative min-w-0 flex-1', 'relative min-w-0 flex-1',
'break-anywhere', // Allow words to break if they are too long. 'break-anywhere', // Allow words to break if they are too long.
@@ -155,7 +150,6 @@ export async function PageBody(props: {
page, page,
}, },
withLinkPreviews, withLinkPreviews,
withCommentAnchors,
isPageBody: true, isPageBody: true,
}} }}
/> />
@@ -255,8 +255,6 @@ export async function PageHeader(props: {
<PageTags page={page} revision={revision} /> <PageTags page={page} revision={revision} />
{page.layout.title ? ( {page.layout.title ? (
<h1 <h1
// Comments on the whole page get their pin next to the title.
{...(context.changeRequest ? { 'data-comment-page-title': '' } : {})}
data-cover-aware-text data-cover-aware-text
data-over-cover data-over-cover
className={tcls( className={tcls(
@@ -1,4 +1,3 @@
import { joinPathWithBaseURL } from '../paths';
import { checkIsHttpURL } from '../urls'; import { checkIsHttpURL } from '../urls';
/** Matches the `feedback` length accepted by `submitSiteAgentFeedback`. */ /** Matches the `feedback` length accepted by `submitSiteAgentFeedback`. */
@@ -14,7 +13,7 @@ export const agentFeedbackDescriptions = {
goal: 'The broader end goal you were ultimately trying to accomplish (as/on behalf of the user) when you hit this issue. Gives the team the context you were working towards. Optional.', goal: 'The broader end goal you were ultimately trying to accomplish (as/on behalf of the user) when you hit this issue. Gives the team the context you were working towards. Optional.',
pageURL: (siteURL: string | undefined) => pageURL: (siteURL: string | undefined) =>
`The full URL of the page the issue is about${siteURL ? ` (e.g. ${joinPathWithBaseURL(siteURL, 'getting-started')})` : ''}, so the finding is linked to the exact page.`, `The full URL of the page the issue is about${siteURL ? ` (e.g. ${siteURL}/getting-started)` : ''}, so the finding is linked to the exact page.`,
pageReference: pageReference:
'The URL of the page the issue is about, so the finding is linked to the exact page. Omit it to report about the page the user is currently viewing.', 'The URL of the page the issue is about, so the finding is linked to the exact page. Omit it to report about the page the user is currently viewing.',
@@ -334,18 +334,6 @@ describe('buildSiteMcpServerCard', () => {
expect(search).not.toHaveProperty('inputSchema'); expect(search).not.toHaveProperty('inputSchema');
}); });
it('gives example page URLs under the path of a subpath site', () => {
const context = makeContext({ host: 'docs.example.com', siteBasePath: '/product/' });
const tools = createSiteMcpTools(context, { request });
const getPage = tools.find((tool) => tool.name === 'getPage');
const pageUrl = tools.find((tool) => tool.name === 'sendFeedback')?.inputSchema.pageUrl;
const example = 'https://docs.example.com/product/getting-started';
expect(getPage?.description).toContain(`(e.g. ${example})`);
expect(pageUrl?.description).toContain(`(e.g. ${example})`);
expect(pageUrl?.parse('getting-started')).toBe(example);
});
it('omits askQuestion when the site has no AI mode', () => { it('omits askQuestion when the site has no AI mode', () => {
const withAI = buildCard({ aiMode: CustomizationAIMode.Assistant }); const withAI = buildCard({ aiMode: CustomizationAIMode.Assistant });
const withoutAI = buildCard({ aiMode: CustomizationAIMode.None }); const withoutAI = buildCard({ aiMode: CustomizationAIMode.None });
+3 -4
View File
@@ -51,7 +51,6 @@ export function createSiteMcpTools(
const { request } = options; const { request } = options;
const { dataFetcher, linker, site } = context; const { dataFetcher, linker, site } = context;
const siteUrl = context.siteSpace.urls.published; const siteUrl = context.siteSpace.urls.published;
const examplePageURL = siteUrl ? joinPathWithBaseURL(siteUrl, 'getting-started') : undefined;
const tools: SiteMcpTool[] = [ const tools: SiteMcpTool[] = [
defineTool({ defineTool({
@@ -147,7 +146,7 @@ export function createSiteMcpTools(
defineTool({ defineTool({
name: 'getPage', name: 'getPage',
description: `Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${examplePageURL}). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`, description: `Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${siteUrl}/getting-started). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`,
inputSchema: { inputSchema: {
url: z url: z
.string() .string()
@@ -161,7 +160,7 @@ export function createSiteMcpTools(
} }
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `"${value}" is not a valid URL. Expected a full URL like ${examplePageURL}`, message: `"${value}" is not a valid URL. Expected a full URL like ${siteUrl}/getting-started`,
}); });
return z.NEVER; return z.NEVER;
}), }),
@@ -357,7 +356,7 @@ export function createSiteMcpTools(
if (!url) { if (!url) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `"${value}" is not a valid URL on this site. Expected a full URL like ${examplePageURL}`, message: `"${value}" is not a valid URL on this site. Expected a full URL like ${siteUrl}/getting-started`,
}); });
return z.NEVER; return z.NEVER;
} }