@athenaintel/react 0.40.2 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/agent-runtime/src/subscription-wake-event.cjs +87 -2
- package/dist/agent-runtime/src/subscription-wake-event.cjs.map +1 -1
- package/dist/agent-runtime/src/subscription-wake-event.js +87 -2
- package/dist/agent-runtime/src/subscription-wake-event.js.map +1 -1
- package/dist/chat/athena-citation-links.cjs +23 -2
- package/dist/chat/athena-citation-links.cjs.map +1 -1
- package/dist/chat/athena-citation-links.d.ts +5 -0
- package/dist/chat/athena-citation-links.js +23 -2
- package/dist/chat/athena-citation-links.js.map +1 -1
- package/dist/chat-ui/index.d.ts +1 -1
- package/dist/chat-ui/mentions/mention-chip.cjs +118 -7
- package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.d.ts +29 -0
- package/dist/chat-ui/mentions/mention-chip.js +120 -9
- package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.cjs +2 -1
- package/dist/chat-ui/rich-text/chat-message-body.cjs.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.js +2 -1
- package/dist/chat-ui/rich-text/chat-message-body.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/references/src/anchors/file-line-range.cjs +70 -0
- package/dist/references/src/anchors/file-line-range.cjs.map +1 -0
- package/dist/references/src/anchors/file-line-range.js +70 -0
- package/dist/references/src/anchors/file-line-range.js.map +1 -0
- package/dist/references/src/anchors/format.cjs +219 -0
- package/dist/references/src/anchors/format.cjs.map +1 -0
- package/dist/references/src/anchors/format.js +219 -0
- package/dist/references/src/anchors/format.js.map +1 -0
- package/dist/references/src/anchors/parse.cjs +408 -0
- package/dist/references/src/anchors/parse.cjs.map +1 -0
- package/dist/references/src/anchors/parse.js +408 -0
- package/dist/references/src/anchors/parse.js.map +1 -0
- package/dist/references/src/anchors/serialize.cjs +185 -0
- package/dist/references/src/anchors/serialize.cjs.map +1 -0
- package/dist/references/src/anchors/serialize.js +185 -0
- package/dist/references/src/anchors/serialize.js.map +1 -0
- package/dist/references/src/anchors/types.cjs +50 -0
- package/dist/references/src/anchors/types.cjs.map +1 -0
- package/dist/references/src/anchors/types.js +50 -0
- package/dist/references/src/anchors/types.js.map +1 -0
- package/dist/references/src/formats/uri/asset-mention-uri.cjs +29 -0
- package/dist/references/src/formats/uri/asset-mention-uri.cjs.map +1 -0
- package/dist/references/src/formats/uri/asset-mention-uri.js +29 -0
- package/dist/references/src/formats/uri/asset-mention-uri.js.map +1 -0
- package/dist/references/src/formats/uri/asset-spaces-url.cjs +64 -0
- package/dist/references/src/formats/uri/asset-spaces-url.cjs.map +1 -0
- package/dist/references/src/formats/uri/asset-spaces-url.js +64 -0
- package/dist/references/src/formats/uri/asset-spaces-url.js.map +1 -0
- package/dist/references/src/formats/uri/template-placeholder.cjs +9 -0
- package/dist/references/src/formats/uri/template-placeholder.cjs.map +1 -0
- package/dist/references/src/formats/uri/template-placeholder.js +9 -0
- package/dist/references/src/formats/uri/template-placeholder.js.map +1 -0
- package/dist/references/src/sheet-name.cjs +10 -0
- package/dist/references/src/sheet-name.cjs.map +1 -0
- package/dist/references/src/sheet-name.js +10 -0
- package/dist/references/src/sheet-name.js.map +1 -0
- package/dist/references/src/version-ref.cjs +61 -0
- package/dist/references/src/version-ref.cjs.map +1 -0
- package/dist/references/src/version-ref.js +61 -0
- package/dist/references/src/version-ref.js.map +1 -0
- package/dist/styles.css +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message-body.cjs","sources":["../../../src/chat-ui/rich-text/chat-message-body.tsx"],"sourcesContent":["import type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\nimport {\n Fragment,\n memo,\n type MouseEvent,\n type ReactNode,\n useCallback,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { useAthenaLinkClickHandler } from \"../../chat/athena-citation-links\";\nimport { type OpenFence, splitChatBlocks } from \"./chat-blocks\";\nimport { ChatLink } from \"./chat-link\";\nimport {\n ChatCodeBlock,\n ChatMention,\n StreamingCodeBlock,\n} from \"./chat-node-views\";\nimport {\n CHAT_TEXT_CLASS,\n parseChatText,\n parseTypedChatText,\n} from \"./chat-text-schema\";\nimport { renderDOMOutputSpec } from \"./dom-output-spec\";\n\nconst subscribe = () => () => {};\nconst browserSnapshot = () => true;\nconst serverSnapshot = () => false;\n\n/**\n * Render the composer's document schema through its own node/mark serializers.\n *\n * The text is split into its top-level blocks (`splitChatBlocks`), each parsed\n * and drawn by its own memoized component under one `CHAT_TEXT_CLASS`\n * wrapper. While a reply streams, only the trailing block re-parses with each\n * token; every finished block keeps its parse and its DOM. The trailing block\n * reveals its newest words with a settle tint and a caret, and a fenced code\n * block still being written renders as plain code until its fence closes.\n *\n * `typed` reads the text as a person typed it (`parseTypedChatText`: raw HTML\n * and underscore emphasis stay the characters they were typed with), for the\n * user bubble; assistant text keeps the full reading.\n */\nexport function ChatMessageBody({\n text,\n isRunning = false,\n typed = false,\n}: {\n text: string;\n isRunning?: boolean;\n typed?: boolean;\n}) {\n const hasDom = useSyncExternalStore(\n subscribe,\n browserSnapshot,\n serverSnapshot,\n );\n const onClick = useChatLinkClick();\n const { blocks, definitions } = useMemo(\n () => splitChatBlocks(text, { streaming: isRunning }),\n [text, isRunning],\n );\n return (\n // The click handler only delegates for the anchors inside, which are\n // themselves focusable and keyboard-operable; the wrapper is not a control.\n <div\n className={CHAT_TEXT_CLASS}\n data-chat-text=\"\"\n data-status={isRunning ? \"running\" : \"complete\"}\n onClick={onClick}\n >\n {hasDom\n ? blocks.map((block, index) => (\n <ChatBlock\n key={index}\n text={block.text}\n definitions={definitions}\n openFence={block.openFence}\n streaming={isRunning && index === blocks.length - 1}\n typed={typed}\n />\n ))\n : text}\n </div>\n );\n}\n\n/**\n * One top-level block. Memoized on its inputs: a finished block's `text` never\n * changes again, so React skips it on every token that lands in the block\n * after it. Only the trailing block carries `streaming` (and, while its code\n * fence is open, `openFence`).\n */\nconst ChatBlock = memo(function ChatBlock({\n text,\n definitions,\n openFence,\n streaming,\n typed,\n}: {\n text: string;\n definitions: string;\n openFence: OpenFence | null;\n streaming: boolean;\n typed: boolean;\n}) {\n // A fence is only \"open\" while the part streams. A message that ended\n // mid-fence parses as the code block it is (markdown-it closes it at the\n // end), copy action and all, with no caret.\n const open = streaming ? openFence : null;\n const doc = useMemo(() => {\n if (open) return null;\n const markdown = definitions ? `${text}\\n\\n${definitions}` : text;\n return typed ? parseTypedChatText(markdown) : parseChatText(markdown);\n }, [text, definitions, open, typed]);\n if (open) {\n return (\n <StreamingCodeBlock\n language={open.language}\n code={open.code}\n caret={<StreamingCaret />}\n />\n );\n }\n // A block of nothing but reference definitions parses to one empty\n // paragraph (the schema's default content); draw nothing for it.\n if (!doc || isEmptyDoc(doc)) return null;\n return <>{renderContent(doc, streaming ? findStreamingTail(doc) : null)}</>;\n});\n\nfunction isEmptyDoc(doc: ProseMirrorNode): boolean {\n if (doc.childCount === 0) return true;\n const only = doc.firstChild;\n return doc.childCount === 1 && only !== null && only.isTextblock && only.content.size === 0;\n}\n\n/**\n * One delegated click handler per body: a click on any rendered anchor —\n * source chip, asset chip, plain link — goes through the SDK's link-click\n * path (`linkClicks` on the provider, then the asset panel for an Athena\n * citation) and is cancelled when that path handled it; otherwise the\n * anchor's own `target=\"_blank\"` navigation runs. Only the native `click`\n * can be cancelled — a `mouseup` handler cannot stop the navigation. A\n * modified click (Cmd/Ctrl for a new tab, Shift for a new window, Alt for a\n * download) is the browser's and is never intercepted.\n */\nfunction useChatLinkClick() {\n const handleLinkClick = useAthenaLinkClickHandler();\n return useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n if (\n event.button !== 0 ||\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n const target = event.target;\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n const anchor = element?.closest(\"a[href]\");\n if (\n !(anchor instanceof HTMLAnchorElement) ||\n !event.currentTarget.contains(anchor)\n ) {\n return;\n }\n const handled = handleLinkClick({\n href: anchor.getAttribute(\"href\") ?? anchor.href,\n linkText: anchor.textContent,\n target: anchor.target,\n nativeEvent: event.nativeEvent,\n });\n if (handled) event.preventDefault();\n },\n [handleLinkClick],\n );\n}\n\n/** Where the block still being written ends: its last text block, and that block's last text node (none inside code). */\ninterface StreamingTail {\n textblock: ProseMirrorNode | null;\n text: ProseMirrorNode | null;\n}\n\nfunction findStreamingTail(doc: ProseMirrorNode): StreamingTail {\n const tail: StreamingTail = { textblock: null, text: null };\n doc.descendants((node) => {\n if (node.isTextblock) {\n tail.textblock = node;\n tail.text = null;\n } else if (\n node.isText &&\n tail.textblock &&\n tail.textblock.type.name !== \"codeBlock\"\n ) {\n tail.text = node;\n }\n return true;\n });\n return tail;\n}\n\n/** How many trailing words keep their own span, so a word's settle animation outlives the tokens after it. */\nconst FRESH_WORD_WINDOW = 6;\nconst WORD = /\\s*\\S+/g;\n\n/**\n * The newest words of the streaming block, each in a span keyed by its\n * position so the same element persists while the word grows and the words\n * after it arrive; a span mounts tinted and settles to ink through\n * `animate-chat-word-settle`. Words that leave the window render as text.\n */\nfunction renderFreshWords(text: string): ReactNode {\n const words = [...text.matchAll(WORD)];\n if (words.length === 0) return text;\n const from = Math.max(0, words.length - FRESH_WORD_WINDOW);\n const first = words[from]!;\n const last = words[words.length - 1]!;\n return (\n <>\n {text.slice(0, first.index)}\n {words.slice(from).map((word, offset) => (\n <span\n key={from + offset}\n className=\"animate-chat-word-settle motion-reduce:animate-none\"\n data-chat-fresh=\"\"\n >\n {word[0]}\n </span>\n ))}\n {text.slice(last.index + last[0].length)}\n </>\n );\n}\n\n/** A thin blinking caret after the newest text while the part streams; no layout of its own beyond a hairline. */\nfunction StreamingCaret() {\n return (\n <span\n aria-hidden=\"true\"\n data-chat-caret=\"\"\n className=\"ml-0.5 inline-block h-[1em] w-0.5 rounded-full bg-primary align-[-0.125em] animate-chat-caret-blink motion-reduce:animate-none\"\n />\n );\n}\n\nfunction renderNode(\n node: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode {\n if (node.isText) {\n return tail?.text === node ? renderFreshWords(node.text ?? \"\") : node.text;\n }\n if (node.type.name === \"mention\") return <ChatMention node={node} />;\n if (node.type.name === \"codeBlock\") {\n return (\n <ChatCodeBlock node={node}>\n <code>\n {node.textContent}\n {tail?.textblock === node ? <StreamingCaret /> : null}\n </code>\n </ChatCodeBlock>\n );\n }\n const toDOM = node.type.spec.toDOM;\n if (!toDOM) return null;\n if (node.isLeaf) return renderDOMOutputSpec(toDOM(node), null);\n const content = renderContent(node, tail);\n if (tail?.textblock === node) content.push(<StreamingCaret key=\"caret\" />);\n return renderDOMOutputSpec(toDOM(node), content);\n}\n\nfunction renderContent(\n parent: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode[] {\n const nodes: ProseMirrorNode[] = [];\n parent.forEach((node) => {\n nodes.push(node);\n });\n return renderMarkRuns(nodes, 0, tail);\n}\n\n/** Match ProseMirror's spanning marks: a link with bold words stays one link. */\nfunction renderMarkRuns(\n nodes: readonly ProseMirrorNode[],\n depth: number,\n tail: StreamingTail | null,\n): ReactNode[] {\n const children: ReactNode[] = [];\n let index = 0;\n while (index < nodes.length) {\n const node = nodes[index]!;\n const mark = node.marks[depth];\n if (!mark) {\n children.push(<Fragment key={index}>{renderNode(node, tail)}</Fragment>);\n index++;\n continue;\n }\n let end = index + 1;\n if (mark.type.spec.spanning !== false) {\n while (end < nodes.length && nodes[end]?.marks[depth]?.eq(mark)) end++;\n }\n const content = renderMarkRuns(nodes.slice(index, end), depth + 1, tail);\n const rendered =\n mark.type.name === \"link\" ? (\n <ChatLink href={mark.attrs.href} excerpt={mark.attrs.excerpt}>\n {content}\n </ChatLink>\n ) : mark.type.spec.toDOM ? (\n renderDOMOutputSpec(mark.type.spec.toDOM(mark, true), content)\n ) : (\n content\n );\n children.push(<Fragment key={index}>{rendered}</Fragment>);\n index = end;\n }\n return children;\n}\n"],"names":["useSyncExternalStore","useMemo","splitChatBlocks","jsx","CHAT_TEXT_CLASS","memo","ChatBlock","parseTypedChatText","parseChatText","StreamingCodeBlock","Fragment","useAthenaLinkClickHandler","useCallback","jsxs","ChatMention","ChatCodeBlock","renderDOMOutputSpec","ChatLink"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-message-body.cjs","sources":["../../../src/chat-ui/rich-text/chat-message-body.tsx"],"sourcesContent":["import type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\nimport {\n Fragment,\n memo,\n type MouseEvent,\n type ReactNode,\n useCallback,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { useAthenaLinkClickHandler } from \"../../chat/athena-citation-links\";\nimport { type OpenFence, splitChatBlocks } from \"./chat-blocks\";\nimport { ChatLink } from \"./chat-link\";\nimport {\n ChatCodeBlock,\n ChatMention,\n StreamingCodeBlock,\n} from \"./chat-node-views\";\nimport {\n CHAT_TEXT_CLASS,\n parseChatText,\n parseTypedChatText,\n} from \"./chat-text-schema\";\nimport { renderDOMOutputSpec } from \"./dom-output-spec\";\n\nconst subscribe = () => () => {};\nconst browserSnapshot = () => true;\nconst serverSnapshot = () => false;\n\n/**\n * Render the composer's document schema through its own node/mark serializers.\n *\n * The text is split into its top-level blocks (`splitChatBlocks`), each parsed\n * and drawn by its own memoized component under one `CHAT_TEXT_CLASS`\n * wrapper. While a reply streams, only the trailing block re-parses with each\n * token; every finished block keeps its parse and its DOM. The trailing block\n * reveals its newest words with a settle tint and a caret, and a fenced code\n * block still being written renders as plain code until its fence closes.\n *\n * `typed` reads the text as a person typed it (`parseTypedChatText`: raw HTML\n * and underscore emphasis stay the characters they were typed with), for the\n * user bubble; assistant text keeps the full reading.\n */\nexport function ChatMessageBody({\n text,\n isRunning = false,\n typed = false,\n}: {\n text: string;\n isRunning?: boolean;\n typed?: boolean;\n}) {\n const hasDom = useSyncExternalStore(\n subscribe,\n browserSnapshot,\n serverSnapshot,\n );\n const onClick = useChatLinkClick();\n const { blocks, definitions } = useMemo(\n () => splitChatBlocks(text, { streaming: isRunning }),\n [text, isRunning],\n );\n return (\n // The click handler only delegates for the anchors inside, which are\n // themselves focusable and keyboard-operable; the wrapper is not a control.\n <div\n className={CHAT_TEXT_CLASS}\n data-chat-text=\"\"\n data-status={isRunning ? \"running\" : \"complete\"}\n onClick={onClick}\n >\n {hasDom\n ? blocks.map((block, index) => (\n <ChatBlock\n key={index}\n text={block.text}\n definitions={definitions}\n openFence={block.openFence}\n streaming={isRunning && index === blocks.length - 1}\n typed={typed}\n />\n ))\n : text}\n </div>\n );\n}\n\n/**\n * One top-level block. Memoized on its inputs: a finished block's `text` never\n * changes again, so React skips it on every token that lands in the block\n * after it. Only the trailing block carries `streaming` (and, while its code\n * fence is open, `openFence`).\n */\nconst ChatBlock = memo(function ChatBlock({\n text,\n definitions,\n openFence,\n streaming,\n typed,\n}: {\n text: string;\n definitions: string;\n openFence: OpenFence | null;\n streaming: boolean;\n typed: boolean;\n}) {\n // A fence is only \"open\" while the part streams. A message that ended\n // mid-fence parses as the code block it is (markdown-it closes it at the\n // end), copy action and all, with no caret.\n const open = streaming ? openFence : null;\n const doc = useMemo(() => {\n if (open) return null;\n const markdown = definitions ? `${text}\\n\\n${definitions}` : text;\n return typed ? parseTypedChatText(markdown) : parseChatText(markdown);\n }, [text, definitions, open, typed]);\n if (open) {\n return (\n <StreamingCodeBlock\n language={open.language}\n code={open.code}\n caret={<StreamingCaret />}\n />\n );\n }\n // A block of nothing but reference definitions parses to one empty\n // paragraph (the schema's default content); draw nothing for it.\n if (!doc || isEmptyDoc(doc)) return null;\n return <>{renderContent(doc, streaming ? findStreamingTail(doc) : null)}</>;\n});\n\nfunction isEmptyDoc(doc: ProseMirrorNode): boolean {\n if (doc.childCount === 0) return true;\n const only = doc.firstChild;\n return doc.childCount === 1 && only !== null && only.isTextblock && only.content.size === 0;\n}\n\n/**\n * One delegated click handler per body: a click on any rendered anchor —\n * source chip, asset chip, plain link — goes through the SDK's link-click\n * path (`linkClicks` on the provider, then the asset panel for an Athena\n * citation) and is cancelled when that path handled it; otherwise the\n * anchor's own `target=\"_blank\"` navigation runs. Only the native `click`\n * can be cancelled — a `mouseup` handler cannot stop the navigation. A\n * modified click (Cmd/Ctrl for a new tab, Shift for a new window, Alt for a\n * download) is the browser's and is never intercepted.\n */\nfunction useChatLinkClick() {\n const handleLinkClick = useAthenaLinkClickHandler();\n return useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n if (\n event.button !== 0 ||\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n const target = event.target;\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n const anchor = element?.closest(\"a[href]\");\n if (\n !(anchor instanceof HTMLAnchorElement) ||\n !event.currentTarget.contains(anchor)\n ) {\n return;\n }\n const handled = handleLinkClick({\n href: anchor.getAttribute(\"href\") ?? anchor.href,\n // A citation chip's text ends with its place (`p.10`); the asset is named without it.\n linkText: anchor.getAttribute(\"data-mention-name\") ?? anchor.textContent,\n target: anchor.target,\n nativeEvent: event.nativeEvent,\n });\n if (handled) event.preventDefault();\n },\n [handleLinkClick],\n );\n}\n\n/** Where the block still being written ends: its last text block, and that block's last text node (none inside code). */\ninterface StreamingTail {\n textblock: ProseMirrorNode | null;\n text: ProseMirrorNode | null;\n}\n\nfunction findStreamingTail(doc: ProseMirrorNode): StreamingTail {\n const tail: StreamingTail = { textblock: null, text: null };\n doc.descendants((node) => {\n if (node.isTextblock) {\n tail.textblock = node;\n tail.text = null;\n } else if (\n node.isText &&\n tail.textblock &&\n tail.textblock.type.name !== \"codeBlock\"\n ) {\n tail.text = node;\n }\n return true;\n });\n return tail;\n}\n\n/** How many trailing words keep their own span, so a word's settle animation outlives the tokens after it. */\nconst FRESH_WORD_WINDOW = 6;\nconst WORD = /\\s*\\S+/g;\n\n/**\n * The newest words of the streaming block, each in a span keyed by its\n * position so the same element persists while the word grows and the words\n * after it arrive; a span mounts tinted and settles to ink through\n * `animate-chat-word-settle`. Words that leave the window render as text.\n */\nfunction renderFreshWords(text: string): ReactNode {\n const words = [...text.matchAll(WORD)];\n if (words.length === 0) return text;\n const from = Math.max(0, words.length - FRESH_WORD_WINDOW);\n const first = words[from]!;\n const last = words[words.length - 1]!;\n return (\n <>\n {text.slice(0, first.index)}\n {words.slice(from).map((word, offset) => (\n <span\n key={from + offset}\n className=\"animate-chat-word-settle motion-reduce:animate-none\"\n data-chat-fresh=\"\"\n >\n {word[0]}\n </span>\n ))}\n {text.slice(last.index + last[0].length)}\n </>\n );\n}\n\n/** A thin blinking caret after the newest text while the part streams; no layout of its own beyond a hairline. */\nfunction StreamingCaret() {\n return (\n <span\n aria-hidden=\"true\"\n data-chat-caret=\"\"\n className=\"ml-0.5 inline-block h-[1em] w-0.5 rounded-full bg-primary align-[-0.125em] animate-chat-caret-blink motion-reduce:animate-none\"\n />\n );\n}\n\nfunction renderNode(\n node: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode {\n if (node.isText) {\n return tail?.text === node ? renderFreshWords(node.text ?? \"\") : node.text;\n }\n if (node.type.name === \"mention\") return <ChatMention node={node} />;\n if (node.type.name === \"codeBlock\") {\n return (\n <ChatCodeBlock node={node}>\n <code>\n {node.textContent}\n {tail?.textblock === node ? <StreamingCaret /> : null}\n </code>\n </ChatCodeBlock>\n );\n }\n const toDOM = node.type.spec.toDOM;\n if (!toDOM) return null;\n if (node.isLeaf) return renderDOMOutputSpec(toDOM(node), null);\n const content = renderContent(node, tail);\n if (tail?.textblock === node) content.push(<StreamingCaret key=\"caret\" />);\n return renderDOMOutputSpec(toDOM(node), content);\n}\n\nfunction renderContent(\n parent: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode[] {\n const nodes: ProseMirrorNode[] = [];\n parent.forEach((node) => {\n nodes.push(node);\n });\n return renderMarkRuns(nodes, 0, tail);\n}\n\n/** Match ProseMirror's spanning marks: a link with bold words stays one link. */\nfunction renderMarkRuns(\n nodes: readonly ProseMirrorNode[],\n depth: number,\n tail: StreamingTail | null,\n): ReactNode[] {\n const children: ReactNode[] = [];\n let index = 0;\n while (index < nodes.length) {\n const node = nodes[index]!;\n const mark = node.marks[depth];\n if (!mark) {\n children.push(<Fragment key={index}>{renderNode(node, tail)}</Fragment>);\n index++;\n continue;\n }\n let end = index + 1;\n if (mark.type.spec.spanning !== false) {\n while (end < nodes.length && nodes[end]?.marks[depth]?.eq(mark)) end++;\n }\n const content = renderMarkRuns(nodes.slice(index, end), depth + 1, tail);\n const rendered =\n mark.type.name === \"link\" ? (\n <ChatLink href={mark.attrs.href} excerpt={mark.attrs.excerpt}>\n {content}\n </ChatLink>\n ) : mark.type.spec.toDOM ? (\n renderDOMOutputSpec(mark.type.spec.toDOM(mark, true), content)\n ) : (\n content\n );\n children.push(<Fragment key={index}>{rendered}</Fragment>);\n index = end;\n }\n return children;\n}\n"],"names":["useSyncExternalStore","useMemo","splitChatBlocks","jsx","CHAT_TEXT_CLASS","memo","ChatBlock","parseTypedChatText","parseChatText","StreamingCodeBlock","Fragment","useAthenaLinkClickHandler","useCallback","jsxs","ChatMention","ChatCodeBlock","renderDOMOutputSpec","ChatLink"],"mappings":"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"}
|
|
@@ -98,7 +98,8 @@ function useChatLinkClick() {
|
|
|
98
98
|
}
|
|
99
99
|
const handled = handleLinkClick({
|
|
100
100
|
href: anchor.getAttribute("href") ?? anchor.href,
|
|
101
|
-
|
|
101
|
+
// A citation chip's text ends with its place (`p.10`); the asset is named without it.
|
|
102
|
+
linkText: anchor.getAttribute("data-mention-name") ?? anchor.textContent,
|
|
102
103
|
target: anchor.target,
|
|
103
104
|
nativeEvent: event.nativeEvent
|
|
104
105
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message-body.js","sources":["../../../src/chat-ui/rich-text/chat-message-body.tsx"],"sourcesContent":["import type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\nimport {\n Fragment,\n memo,\n type MouseEvent,\n type ReactNode,\n useCallback,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { useAthenaLinkClickHandler } from \"../../chat/athena-citation-links\";\nimport { type OpenFence, splitChatBlocks } from \"./chat-blocks\";\nimport { ChatLink } from \"./chat-link\";\nimport {\n ChatCodeBlock,\n ChatMention,\n StreamingCodeBlock,\n} from \"./chat-node-views\";\nimport {\n CHAT_TEXT_CLASS,\n parseChatText,\n parseTypedChatText,\n} from \"./chat-text-schema\";\nimport { renderDOMOutputSpec } from \"./dom-output-spec\";\n\nconst subscribe = () => () => {};\nconst browserSnapshot = () => true;\nconst serverSnapshot = () => false;\n\n/**\n * Render the composer's document schema through its own node/mark serializers.\n *\n * The text is split into its top-level blocks (`splitChatBlocks`), each parsed\n * and drawn by its own memoized component under one `CHAT_TEXT_CLASS`\n * wrapper. While a reply streams, only the trailing block re-parses with each\n * token; every finished block keeps its parse and its DOM. The trailing block\n * reveals its newest words with a settle tint and a caret, and a fenced code\n * block still being written renders as plain code until its fence closes.\n *\n * `typed` reads the text as a person typed it (`parseTypedChatText`: raw HTML\n * and underscore emphasis stay the characters they were typed with), for the\n * user bubble; assistant text keeps the full reading.\n */\nexport function ChatMessageBody({\n text,\n isRunning = false,\n typed = false,\n}: {\n text: string;\n isRunning?: boolean;\n typed?: boolean;\n}) {\n const hasDom = useSyncExternalStore(\n subscribe,\n browserSnapshot,\n serverSnapshot,\n );\n const onClick = useChatLinkClick();\n const { blocks, definitions } = useMemo(\n () => splitChatBlocks(text, { streaming: isRunning }),\n [text, isRunning],\n );\n return (\n // The click handler only delegates for the anchors inside, which are\n // themselves focusable and keyboard-operable; the wrapper is not a control.\n <div\n className={CHAT_TEXT_CLASS}\n data-chat-text=\"\"\n data-status={isRunning ? \"running\" : \"complete\"}\n onClick={onClick}\n >\n {hasDom\n ? blocks.map((block, index) => (\n <ChatBlock\n key={index}\n text={block.text}\n definitions={definitions}\n openFence={block.openFence}\n streaming={isRunning && index === blocks.length - 1}\n typed={typed}\n />\n ))\n : text}\n </div>\n );\n}\n\n/**\n * One top-level block. Memoized on its inputs: a finished block's `text` never\n * changes again, so React skips it on every token that lands in the block\n * after it. Only the trailing block carries `streaming` (and, while its code\n * fence is open, `openFence`).\n */\nconst ChatBlock = memo(function ChatBlock({\n text,\n definitions,\n openFence,\n streaming,\n typed,\n}: {\n text: string;\n definitions: string;\n openFence: OpenFence | null;\n streaming: boolean;\n typed: boolean;\n}) {\n // A fence is only \"open\" while the part streams. A message that ended\n // mid-fence parses as the code block it is (markdown-it closes it at the\n // end), copy action and all, with no caret.\n const open = streaming ? openFence : null;\n const doc = useMemo(() => {\n if (open) return null;\n const markdown = definitions ? `${text}\\n\\n${definitions}` : text;\n return typed ? parseTypedChatText(markdown) : parseChatText(markdown);\n }, [text, definitions, open, typed]);\n if (open) {\n return (\n <StreamingCodeBlock\n language={open.language}\n code={open.code}\n caret={<StreamingCaret />}\n />\n );\n }\n // A block of nothing but reference definitions parses to one empty\n // paragraph (the schema's default content); draw nothing for it.\n if (!doc || isEmptyDoc(doc)) return null;\n return <>{renderContent(doc, streaming ? findStreamingTail(doc) : null)}</>;\n});\n\nfunction isEmptyDoc(doc: ProseMirrorNode): boolean {\n if (doc.childCount === 0) return true;\n const only = doc.firstChild;\n return doc.childCount === 1 && only !== null && only.isTextblock && only.content.size === 0;\n}\n\n/**\n * One delegated click handler per body: a click on any rendered anchor —\n * source chip, asset chip, plain link — goes through the SDK's link-click\n * path (`linkClicks` on the provider, then the asset panel for an Athena\n * citation) and is cancelled when that path handled it; otherwise the\n * anchor's own `target=\"_blank\"` navigation runs. Only the native `click`\n * can be cancelled — a `mouseup` handler cannot stop the navigation. A\n * modified click (Cmd/Ctrl for a new tab, Shift for a new window, Alt for a\n * download) is the browser's and is never intercepted.\n */\nfunction useChatLinkClick() {\n const handleLinkClick = useAthenaLinkClickHandler();\n return useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n if (\n event.button !== 0 ||\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n const target = event.target;\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n const anchor = element?.closest(\"a[href]\");\n if (\n !(anchor instanceof HTMLAnchorElement) ||\n !event.currentTarget.contains(anchor)\n ) {\n return;\n }\n const handled = handleLinkClick({\n href: anchor.getAttribute(\"href\") ?? anchor.href,\n linkText: anchor.textContent,\n target: anchor.target,\n nativeEvent: event.nativeEvent,\n });\n if (handled) event.preventDefault();\n },\n [handleLinkClick],\n );\n}\n\n/** Where the block still being written ends: its last text block, and that block's last text node (none inside code). */\ninterface StreamingTail {\n textblock: ProseMirrorNode | null;\n text: ProseMirrorNode | null;\n}\n\nfunction findStreamingTail(doc: ProseMirrorNode): StreamingTail {\n const tail: StreamingTail = { textblock: null, text: null };\n doc.descendants((node) => {\n if (node.isTextblock) {\n tail.textblock = node;\n tail.text = null;\n } else if (\n node.isText &&\n tail.textblock &&\n tail.textblock.type.name !== \"codeBlock\"\n ) {\n tail.text = node;\n }\n return true;\n });\n return tail;\n}\n\n/** How many trailing words keep their own span, so a word's settle animation outlives the tokens after it. */\nconst FRESH_WORD_WINDOW = 6;\nconst WORD = /\\s*\\S+/g;\n\n/**\n * The newest words of the streaming block, each in a span keyed by its\n * position so the same element persists while the word grows and the words\n * after it arrive; a span mounts tinted and settles to ink through\n * `animate-chat-word-settle`. Words that leave the window render as text.\n */\nfunction renderFreshWords(text: string): ReactNode {\n const words = [...text.matchAll(WORD)];\n if (words.length === 0) return text;\n const from = Math.max(0, words.length - FRESH_WORD_WINDOW);\n const first = words[from]!;\n const last = words[words.length - 1]!;\n return (\n <>\n {text.slice(0, first.index)}\n {words.slice(from).map((word, offset) => (\n <span\n key={from + offset}\n className=\"animate-chat-word-settle motion-reduce:animate-none\"\n data-chat-fresh=\"\"\n >\n {word[0]}\n </span>\n ))}\n {text.slice(last.index + last[0].length)}\n </>\n );\n}\n\n/** A thin blinking caret after the newest text while the part streams; no layout of its own beyond a hairline. */\nfunction StreamingCaret() {\n return (\n <span\n aria-hidden=\"true\"\n data-chat-caret=\"\"\n className=\"ml-0.5 inline-block h-[1em] w-0.5 rounded-full bg-primary align-[-0.125em] animate-chat-caret-blink motion-reduce:animate-none\"\n />\n );\n}\n\nfunction renderNode(\n node: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode {\n if (node.isText) {\n return tail?.text === node ? renderFreshWords(node.text ?? \"\") : node.text;\n }\n if (node.type.name === \"mention\") return <ChatMention node={node} />;\n if (node.type.name === \"codeBlock\") {\n return (\n <ChatCodeBlock node={node}>\n <code>\n {node.textContent}\n {tail?.textblock === node ? <StreamingCaret /> : null}\n </code>\n </ChatCodeBlock>\n );\n }\n const toDOM = node.type.spec.toDOM;\n if (!toDOM) return null;\n if (node.isLeaf) return renderDOMOutputSpec(toDOM(node), null);\n const content = renderContent(node, tail);\n if (tail?.textblock === node) content.push(<StreamingCaret key=\"caret\" />);\n return renderDOMOutputSpec(toDOM(node), content);\n}\n\nfunction renderContent(\n parent: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode[] {\n const nodes: ProseMirrorNode[] = [];\n parent.forEach((node) => {\n nodes.push(node);\n });\n return renderMarkRuns(nodes, 0, tail);\n}\n\n/** Match ProseMirror's spanning marks: a link with bold words stays one link. */\nfunction renderMarkRuns(\n nodes: readonly ProseMirrorNode[],\n depth: number,\n tail: StreamingTail | null,\n): ReactNode[] {\n const children: ReactNode[] = [];\n let index = 0;\n while (index < nodes.length) {\n const node = nodes[index]!;\n const mark = node.marks[depth];\n if (!mark) {\n children.push(<Fragment key={index}>{renderNode(node, tail)}</Fragment>);\n index++;\n continue;\n }\n let end = index + 1;\n if (mark.type.spec.spanning !== false) {\n while (end < nodes.length && nodes[end]?.marks[depth]?.eq(mark)) end++;\n }\n const content = renderMarkRuns(nodes.slice(index, end), depth + 1, tail);\n const rendered =\n mark.type.name === \"link\" ? (\n <ChatLink href={mark.attrs.href} excerpt={mark.attrs.excerpt}>\n {content}\n </ChatLink>\n ) : mark.type.spec.toDOM ? (\n renderDOMOutputSpec(mark.type.spec.toDOM(mark, true), content)\n ) : (\n content\n );\n children.push(<Fragment key={index}>{rendered}</Fragment>);\n index = end;\n }\n return children;\n}\n"],"names":["ChatBlock","Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-message-body.js","sources":["../../../src/chat-ui/rich-text/chat-message-body.tsx"],"sourcesContent":["import type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\nimport {\n Fragment,\n memo,\n type MouseEvent,\n type ReactNode,\n useCallback,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { useAthenaLinkClickHandler } from \"../../chat/athena-citation-links\";\nimport { type OpenFence, splitChatBlocks } from \"./chat-blocks\";\nimport { ChatLink } from \"./chat-link\";\nimport {\n ChatCodeBlock,\n ChatMention,\n StreamingCodeBlock,\n} from \"./chat-node-views\";\nimport {\n CHAT_TEXT_CLASS,\n parseChatText,\n parseTypedChatText,\n} from \"./chat-text-schema\";\nimport { renderDOMOutputSpec } from \"./dom-output-spec\";\n\nconst subscribe = () => () => {};\nconst browserSnapshot = () => true;\nconst serverSnapshot = () => false;\n\n/**\n * Render the composer's document schema through its own node/mark serializers.\n *\n * The text is split into its top-level blocks (`splitChatBlocks`), each parsed\n * and drawn by its own memoized component under one `CHAT_TEXT_CLASS`\n * wrapper. While a reply streams, only the trailing block re-parses with each\n * token; every finished block keeps its parse and its DOM. The trailing block\n * reveals its newest words with a settle tint and a caret, and a fenced code\n * block still being written renders as plain code until its fence closes.\n *\n * `typed` reads the text as a person typed it (`parseTypedChatText`: raw HTML\n * and underscore emphasis stay the characters they were typed with), for the\n * user bubble; assistant text keeps the full reading.\n */\nexport function ChatMessageBody({\n text,\n isRunning = false,\n typed = false,\n}: {\n text: string;\n isRunning?: boolean;\n typed?: boolean;\n}) {\n const hasDom = useSyncExternalStore(\n subscribe,\n browserSnapshot,\n serverSnapshot,\n );\n const onClick = useChatLinkClick();\n const { blocks, definitions } = useMemo(\n () => splitChatBlocks(text, { streaming: isRunning }),\n [text, isRunning],\n );\n return (\n // The click handler only delegates for the anchors inside, which are\n // themselves focusable and keyboard-operable; the wrapper is not a control.\n <div\n className={CHAT_TEXT_CLASS}\n data-chat-text=\"\"\n data-status={isRunning ? \"running\" : \"complete\"}\n onClick={onClick}\n >\n {hasDom\n ? blocks.map((block, index) => (\n <ChatBlock\n key={index}\n text={block.text}\n definitions={definitions}\n openFence={block.openFence}\n streaming={isRunning && index === blocks.length - 1}\n typed={typed}\n />\n ))\n : text}\n </div>\n );\n}\n\n/**\n * One top-level block. Memoized on its inputs: a finished block's `text` never\n * changes again, so React skips it on every token that lands in the block\n * after it. Only the trailing block carries `streaming` (and, while its code\n * fence is open, `openFence`).\n */\nconst ChatBlock = memo(function ChatBlock({\n text,\n definitions,\n openFence,\n streaming,\n typed,\n}: {\n text: string;\n definitions: string;\n openFence: OpenFence | null;\n streaming: boolean;\n typed: boolean;\n}) {\n // A fence is only \"open\" while the part streams. A message that ended\n // mid-fence parses as the code block it is (markdown-it closes it at the\n // end), copy action and all, with no caret.\n const open = streaming ? openFence : null;\n const doc = useMemo(() => {\n if (open) return null;\n const markdown = definitions ? `${text}\\n\\n${definitions}` : text;\n return typed ? parseTypedChatText(markdown) : parseChatText(markdown);\n }, [text, definitions, open, typed]);\n if (open) {\n return (\n <StreamingCodeBlock\n language={open.language}\n code={open.code}\n caret={<StreamingCaret />}\n />\n );\n }\n // A block of nothing but reference definitions parses to one empty\n // paragraph (the schema's default content); draw nothing for it.\n if (!doc || isEmptyDoc(doc)) return null;\n return <>{renderContent(doc, streaming ? findStreamingTail(doc) : null)}</>;\n});\n\nfunction isEmptyDoc(doc: ProseMirrorNode): boolean {\n if (doc.childCount === 0) return true;\n const only = doc.firstChild;\n return doc.childCount === 1 && only !== null && only.isTextblock && only.content.size === 0;\n}\n\n/**\n * One delegated click handler per body: a click on any rendered anchor —\n * source chip, asset chip, plain link — goes through the SDK's link-click\n * path (`linkClicks` on the provider, then the asset panel for an Athena\n * citation) and is cancelled when that path handled it; otherwise the\n * anchor's own `target=\"_blank\"` navigation runs. Only the native `click`\n * can be cancelled — a `mouseup` handler cannot stop the navigation. A\n * modified click (Cmd/Ctrl for a new tab, Shift for a new window, Alt for a\n * download) is the browser's and is never intercepted.\n */\nfunction useChatLinkClick() {\n const handleLinkClick = useAthenaLinkClickHandler();\n return useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n if (\n event.button !== 0 ||\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n const target = event.target;\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n const anchor = element?.closest(\"a[href]\");\n if (\n !(anchor instanceof HTMLAnchorElement) ||\n !event.currentTarget.contains(anchor)\n ) {\n return;\n }\n const handled = handleLinkClick({\n href: anchor.getAttribute(\"href\") ?? anchor.href,\n // A citation chip's text ends with its place (`p.10`); the asset is named without it.\n linkText: anchor.getAttribute(\"data-mention-name\") ?? anchor.textContent,\n target: anchor.target,\n nativeEvent: event.nativeEvent,\n });\n if (handled) event.preventDefault();\n },\n [handleLinkClick],\n );\n}\n\n/** Where the block still being written ends: its last text block, and that block's last text node (none inside code). */\ninterface StreamingTail {\n textblock: ProseMirrorNode | null;\n text: ProseMirrorNode | null;\n}\n\nfunction findStreamingTail(doc: ProseMirrorNode): StreamingTail {\n const tail: StreamingTail = { textblock: null, text: null };\n doc.descendants((node) => {\n if (node.isTextblock) {\n tail.textblock = node;\n tail.text = null;\n } else if (\n node.isText &&\n tail.textblock &&\n tail.textblock.type.name !== \"codeBlock\"\n ) {\n tail.text = node;\n }\n return true;\n });\n return tail;\n}\n\n/** How many trailing words keep their own span, so a word's settle animation outlives the tokens after it. */\nconst FRESH_WORD_WINDOW = 6;\nconst WORD = /\\s*\\S+/g;\n\n/**\n * The newest words of the streaming block, each in a span keyed by its\n * position so the same element persists while the word grows and the words\n * after it arrive; a span mounts tinted and settles to ink through\n * `animate-chat-word-settle`. Words that leave the window render as text.\n */\nfunction renderFreshWords(text: string): ReactNode {\n const words = [...text.matchAll(WORD)];\n if (words.length === 0) return text;\n const from = Math.max(0, words.length - FRESH_WORD_WINDOW);\n const first = words[from]!;\n const last = words[words.length - 1]!;\n return (\n <>\n {text.slice(0, first.index)}\n {words.slice(from).map((word, offset) => (\n <span\n key={from + offset}\n className=\"animate-chat-word-settle motion-reduce:animate-none\"\n data-chat-fresh=\"\"\n >\n {word[0]}\n </span>\n ))}\n {text.slice(last.index + last[0].length)}\n </>\n );\n}\n\n/** A thin blinking caret after the newest text while the part streams; no layout of its own beyond a hairline. */\nfunction StreamingCaret() {\n return (\n <span\n aria-hidden=\"true\"\n data-chat-caret=\"\"\n className=\"ml-0.5 inline-block h-[1em] w-0.5 rounded-full bg-primary align-[-0.125em] animate-chat-caret-blink motion-reduce:animate-none\"\n />\n );\n}\n\nfunction renderNode(\n node: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode {\n if (node.isText) {\n return tail?.text === node ? renderFreshWords(node.text ?? \"\") : node.text;\n }\n if (node.type.name === \"mention\") return <ChatMention node={node} />;\n if (node.type.name === \"codeBlock\") {\n return (\n <ChatCodeBlock node={node}>\n <code>\n {node.textContent}\n {tail?.textblock === node ? <StreamingCaret /> : null}\n </code>\n </ChatCodeBlock>\n );\n }\n const toDOM = node.type.spec.toDOM;\n if (!toDOM) return null;\n if (node.isLeaf) return renderDOMOutputSpec(toDOM(node), null);\n const content = renderContent(node, tail);\n if (tail?.textblock === node) content.push(<StreamingCaret key=\"caret\" />);\n return renderDOMOutputSpec(toDOM(node), content);\n}\n\nfunction renderContent(\n parent: ProseMirrorNode,\n tail: StreamingTail | null,\n): ReactNode[] {\n const nodes: ProseMirrorNode[] = [];\n parent.forEach((node) => {\n nodes.push(node);\n });\n return renderMarkRuns(nodes, 0, tail);\n}\n\n/** Match ProseMirror's spanning marks: a link with bold words stays one link. */\nfunction renderMarkRuns(\n nodes: readonly ProseMirrorNode[],\n depth: number,\n tail: StreamingTail | null,\n): ReactNode[] {\n const children: ReactNode[] = [];\n let index = 0;\n while (index < nodes.length) {\n const node = nodes[index]!;\n const mark = node.marks[depth];\n if (!mark) {\n children.push(<Fragment key={index}>{renderNode(node, tail)}</Fragment>);\n index++;\n continue;\n }\n let end = index + 1;\n if (mark.type.spec.spanning !== false) {\n while (end < nodes.length && nodes[end]?.marks[depth]?.eq(mark)) end++;\n }\n const content = renderMarkRuns(nodes.slice(index, end), depth + 1, tail);\n const rendered =\n mark.type.name === \"link\" ? (\n <ChatLink href={mark.attrs.href} excerpt={mark.attrs.excerpt}>\n {content}\n </ChatLink>\n ) : mark.type.spec.toDOM ? (\n renderDOMOutputSpec(mark.type.spec.toDOM(mark, true), content)\n ) : (\n content\n );\n children.push(<Fragment key={index}>{rendered}</Fragment>);\n index = end;\n }\n return children;\n}\n"],"names":["ChatBlock","Fragment"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const FILE_LINE_RANGE_BOUNDS = {
|
|
4
|
+
/** Longest repo-relative path, counted in Unicode code points (Python `len`). */
|
|
5
|
+
MAX_REPO_PATH_LENGTH: 1024,
|
|
6
|
+
/** Largest 1-based line number either end of the range may name. */
|
|
7
|
+
MAX_LINE_NUMBER: 1e7,
|
|
8
|
+
/** Largest canonical anchor, in UTF-8 bytes, that fits Clio's store. */
|
|
9
|
+
MAX_ANCHOR_BYTES: 2048
|
|
10
|
+
};
|
|
11
|
+
const LONE_SURROGATE = new RegExp("[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]");
|
|
12
|
+
function repoPathProblem(path) {
|
|
13
|
+
if (path.length === 0) return "path must be non-empty";
|
|
14
|
+
if (Array.from(path).length > FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH) {
|
|
15
|
+
return `path must be at most ${FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH} characters`;
|
|
16
|
+
}
|
|
17
|
+
if (LONE_SURROGATE.test(path)) return "path must be valid UTF-8 (no lone surrogates)";
|
|
18
|
+
if (path.includes("\0")) return "path must not contain NUL";
|
|
19
|
+
if (path.startsWith("/")) return "path must be repo-relative (no leading '/')";
|
|
20
|
+
if (path.endsWith("/")) return "path must name a file (no trailing '/')";
|
|
21
|
+
for (const segment of path.split("/")) {
|
|
22
|
+
if (segment.length === 0) return "path must not contain an empty segment";
|
|
23
|
+
if (segment === "." || segment === "..") return "path must not contain '.' or '..' segments";
|
|
24
|
+
}
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
function pythonFormEncode(value) {
|
|
28
|
+
let out = "";
|
|
29
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
30
|
+
const unreserved = byte >= 48 && byte <= 57 || // 0-9
|
|
31
|
+
byte >= 65 && byte <= 90 || // A-Z
|
|
32
|
+
byte >= 97 && byte <= 122 || // a-z
|
|
33
|
+
byte === 45 || // -
|
|
34
|
+
byte === 46 || // .
|
|
35
|
+
byte === 95 || // _
|
|
36
|
+
byte === 126;
|
|
37
|
+
if (unreserved) {
|
|
38
|
+
out += String.fromCharCode(byte);
|
|
39
|
+
} else if (byte === 32) {
|
|
40
|
+
out += "+";
|
|
41
|
+
} else {
|
|
42
|
+
out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function fileLineRangeAnchorByteLength(anchor) {
|
|
48
|
+
return `anchor=file_line_range&end_line=${anchor.endLine}&path=${pythonFormEncode(anchor.path)}&start_line=${anchor.startLine}`.length;
|
|
49
|
+
}
|
|
50
|
+
function lineNumberProblem(name, value) {
|
|
51
|
+
if (!Number.isInteger(value) || value < 1 || value > FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER) {
|
|
52
|
+
return `${name} must be an integer between 1 and ${FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER}`;
|
|
53
|
+
}
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
function fileLineRangeAnchorProblem(anchor) {
|
|
57
|
+
const problem = repoPathProblem(anchor.path) ?? lineNumberProblem("start_line", anchor.startLine) ?? lineNumberProblem("end_line", anchor.endLine);
|
|
58
|
+
if (problem) return problem;
|
|
59
|
+
if (anchor.endLine < anchor.startLine) return "end_line must be >= start_line";
|
|
60
|
+
const length = fileLineRangeAnchorByteLength(anchor);
|
|
61
|
+
if (length > FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES) {
|
|
62
|
+
return `the file_line_range anchor would be ${length} bytes; at most ${FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES} fit the store — shorten the path`;
|
|
63
|
+
}
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
exports.FILE_LINE_RANGE_BOUNDS = FILE_LINE_RANGE_BOUNDS;
|
|
67
|
+
exports.fileLineRangeAnchorByteLength = fileLineRangeAnchorByteLength;
|
|
68
|
+
exports.fileLineRangeAnchorProblem = fileLineRangeAnchorProblem;
|
|
69
|
+
exports.repoPathProblem = repoPathProblem;
|
|
70
|
+
//# sourceMappingURL=file-line-range.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-line-range.cjs","sources":["../../../../../references/src/anchors/file-line-range.ts"],"sourcesContent":["/**\n * Validation rules for the `file_line_range` anchor — an inclusive, 1-based\n * line range of one file of a repository source (a GitHub `catalog` asset\n * whose files Clio indexes; `docs/clio/21-repository-source.md` §2.1).\n *\n * The rules mirror Clio's `LineRangeLocator` / `validate_repo_path`\n * (`clio/clio/contracts/search.py`) and agora's copy of them\n * (`agora/agora/services/clio/contracts.py`) rule for rule: the same bounds,\n * the same path grammar, and a byte cap measured on the canonical anchor Clio\n * writes — `anchor=file_line_range&end_line=<m>&path=<path>&start_line=<n>`\n * (parameters sorted), form-encoded by Python's `urllib.parse.urlencode`.\n * The builder (`serialize.ts`), the parser (`parse.ts`) and the storage-boundary\n * schema (`schemas.ts`) all read these one rules, so an anchor either fits the\n * store on every side or is refused on every side.\n *\n * Wire contract for what TypeScript emits: `serializeAnchorToParams` sorts the\n * parameters like Clio, but `URLSearchParams` percent-encodes `~` (Clio keeps\n * it) and keeps `*` (Clio writes `%2A`), so for those two characters the two\n * strings differ while decoding to the same anchor. Canonical *string*\n * equality is Clio's own concern — only Clio writes its store, and TS strings\n * never enter it; the TS side guarantees decoded-value equality (both strings\n * parse to one anchor) and a byte cap measured on Clio's encoding\n * (`fileLineRangeAnchorByteLength`).\n */\n\nexport const FILE_LINE_RANGE_BOUNDS = {\n /** Longest repo-relative path, counted in Unicode code points (Python `len`). */\n MAX_REPO_PATH_LENGTH: 1024,\n /** Largest 1-based line number either end of the range may name. */\n MAX_LINE_NUMBER: 10_000_000,\n /** Largest canonical anchor, in UTF-8 bytes, that fits Clio's store. */\n MAX_ANCHOR_BYTES: 2048,\n} as const;\n\n/** The fields of a `file_line_range` anchor (see `FileLineRangeAnchor` in `types.ts`). */\nexport type FileLineRangeFields = { path: string; startLine: number; endLine: number };\n\n/** A UTF-16 surrogate half without its partner — not representable as UTF-8. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Why `path` is not a repo-relative, `/`-separated file path, or `undefined`\n * when it is one. Rule for rule the Python `validate_repo_path`: non-empty, at\n * most `MAX_REPO_PATH_LENGTH` code points, valid UTF-8 (no lone surrogate), no\n * NUL, no leading or trailing `/`, no empty, `.` or `..` segment.\n */\nexport function repoPathProblem(path: string): string | undefined {\n if (path.length === 0) return 'path must be non-empty';\n if (Array.from(path).length > FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH) {\n return `path must be at most ${FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH} characters`;\n }\n if (LONE_SURROGATE.test(path)) return 'path must be valid UTF-8 (no lone surrogates)';\n if (path.includes('\\x00')) return 'path must not contain NUL';\n if (path.startsWith('/')) return \"path must be repo-relative (no leading '/')\";\n if (path.endsWith('/')) return \"path must name a file (no trailing '/')\";\n for (const segment of path.split('/')) {\n if (segment.length === 0) return 'path must not contain an empty segment';\n if (segment === '.' || segment === '..') return \"path must not contain '.' or '..' segments\";\n }\n return undefined;\n}\n\n/**\n * One value as Python's `urllib.parse.urlencode` (`quote_plus`) writes it —\n * the bytes that actually sit in Clio's store. `URLSearchParams` encodes the\n * same way except for `~` (kept here, `%7E` there) and `*` (`%2A` here, kept\n * there), so the byte cap is measured with this encoder, never with\n * `URLSearchParams.toString()`.\n */\nfunction pythonFormEncode(value: string): string {\n let out = '';\n for (const byte of new TextEncoder().encode(value)) {\n const unreserved =\n (byte >= 0x30 && byte <= 0x39) || // 0-9\n (byte >= 0x41 && byte <= 0x5a) || // A-Z\n (byte >= 0x61 && byte <= 0x7a) || // a-z\n byte === 0x2d || // -\n byte === 0x2e || // .\n byte === 0x5f || // _\n byte === 0x7e; // ~\n if (unreserved) {\n out += String.fromCharCode(byte);\n } else if (byte === 0x20) {\n out += '+';\n } else {\n out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n }\n return out;\n}\n\n/**\n * UTF-8 byte length of the canonical anchor Clio writes for these fields\n * (`anchor=file_line_range&end_line=<m>&path=<path>&start_line=<n>`, ASCII).\n * Equals `LineRangeLocator.anchor_length()` on the Python sides.\n */\nexport function fileLineRangeAnchorByteLength(anchor: FileLineRangeFields): number {\n return `anchor=file_line_range&end_line=${anchor.endLine}&path=${pythonFormEncode(anchor.path)}&start_line=${anchor.startLine}`\n .length;\n}\n\nfunction lineNumberProblem(name: string, value: number): string | undefined {\n if (\n !Number.isInteger(value) ||\n value < 1 ||\n value > FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER\n ) {\n return `${name} must be an integer between 1 and ${FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER}`;\n }\n return undefined;\n}\n\n/**\n * Why `anchor` is not a storable `file_line_range` anchor, or `undefined` when\n * it is one. Rule for rule the Python `LineRangeLocator`: the repo-path\n * grammar (`repoPathProblem`), both lines integers in `[1, MAX_LINE_NUMBER]`,\n * `endLine >= startLine`, and the canonical anchor at most `MAX_ANCHOR_BYTES`.\n */\nexport function fileLineRangeAnchorProblem(anchor: FileLineRangeFields): string | undefined {\n const problem =\n repoPathProblem(anchor.path) ??\n lineNumberProblem('start_line', anchor.startLine) ??\n lineNumberProblem('end_line', anchor.endLine);\n if (problem) return problem;\n if (anchor.endLine < anchor.startLine) return 'end_line must be >= start_line';\n const length = fileLineRangeAnchorByteLength(anchor);\n if (length > FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES) {\n return `the file_line_range anchor would be ${length} bytes; at most ${FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES} fit the store — shorten the path`;\n }\n return undefined;\n}\n"],"names":[],"mappings":";;AAyBO,MAAM,yBAAyB;AAAA;AAAA,EAEpC,sBAAsB;AAAA;AAAA,EAEtB,iBAAiB;AAAA;AAAA,EAEjB,kBAAkB;AACpB;AAMA,MAAM,iBAAiB,WAAA,gFAAA;AAQhB,SAAS,gBAAgB,MAAkC;AAChE,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,MAAM,KAAK,IAAI,EAAE,SAAS,uBAAuB,sBAAsB;AACzE,WAAO,wBAAwB,uBAAuB,oBAAoB;AAAA,EAC5E;AACA,MAAI,eAAe,KAAK,IAAI,EAAG,QAAO;AACtC,MAAI,KAAK,SAAS,IAAM,EAAG,QAAO;AAClC,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AACjC,MAAI,KAAK,SAAS,GAAG,EAAG,QAAO;AAC/B,aAAW,WAAW,KAAK,MAAM,GAAG,GAAG;AACrC,QAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAI,YAAY,OAAO,YAAY,KAAM,QAAO;AAAA,EAClD;AACA,SAAO;AACT;AASA,SAAS,iBAAiB,OAAuB;AAC/C,MAAI,MAAM;AACV,aAAW,QAAQ,IAAI,YAAA,EAAc,OAAO,KAAK,GAAG;AAClD,UAAM,aACH,QAAQ,MAAQ,QAAQ;AAAA,IACxB,QAAQ,MAAQ,QAAQ;AAAA,IACxB,QAAQ,MAAQ,QAAQ;AAAA,IACzB,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AACX,QAAI,YAAY;AACd,aAAO,OAAO,aAAa,IAAI;AAAA,IACjC,WAAW,SAAS,IAAM;AACxB,aAAO;AAAA,IACT,OAAO;AACL,aAAO,IAAI,KAAK,SAAS,EAAE,EAAE,cAAc,SAAS,GAAG,GAAG,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,8BAA8B,QAAqC;AACjF,SAAO,mCAAmC,OAAO,OAAO,SAAS,iBAAiB,OAAO,IAAI,CAAC,eAAe,OAAO,SAAS,GAC1H;AACL;AAEA,SAAS,kBAAkB,MAAc,OAAmC;AAC1E,MACE,CAAC,OAAO,UAAU,KAAK,KACvB,QAAQ,KACR,QAAQ,uBAAuB,iBAC/B;AACA,WAAO,GAAG,IAAI,qCAAqC,uBAAuB,eAAe;AAAA,EAC3F;AACA,SAAO;AACT;AAQO,SAAS,2BAA2B,QAAiD;AAC1F,QAAM,UACJ,gBAAgB,OAAO,IAAI,KAC3B,kBAAkB,cAAc,OAAO,SAAS,KAChD,kBAAkB,YAAY,OAAO,OAAO;AAC9C,MAAI,QAAS,QAAO;AACpB,MAAI,OAAO,UAAU,OAAO,UAAW,QAAO;AAC9C,QAAM,SAAS,8BAA8B,MAAM;AACnD,MAAI,SAAS,uBAAuB,kBAAkB;AACpD,WAAO,uCAAuC,MAAM,mBAAmB,uBAAuB,gBAAgB;AAAA,EAChH;AACA,SAAO;AACT;;;;;"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const FILE_LINE_RANGE_BOUNDS = {
|
|
2
|
+
/** Longest repo-relative path, counted in Unicode code points (Python `len`). */
|
|
3
|
+
MAX_REPO_PATH_LENGTH: 1024,
|
|
4
|
+
/** Largest 1-based line number either end of the range may name. */
|
|
5
|
+
MAX_LINE_NUMBER: 1e7,
|
|
6
|
+
/** Largest canonical anchor, in UTF-8 bytes, that fits Clio's store. */
|
|
7
|
+
MAX_ANCHOR_BYTES: 2048
|
|
8
|
+
};
|
|
9
|
+
const LONE_SURROGATE = new RegExp("[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]");
|
|
10
|
+
function repoPathProblem(path) {
|
|
11
|
+
if (path.length === 0) return "path must be non-empty";
|
|
12
|
+
if (Array.from(path).length > FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH) {
|
|
13
|
+
return `path must be at most ${FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH} characters`;
|
|
14
|
+
}
|
|
15
|
+
if (LONE_SURROGATE.test(path)) return "path must be valid UTF-8 (no lone surrogates)";
|
|
16
|
+
if (path.includes("\0")) return "path must not contain NUL";
|
|
17
|
+
if (path.startsWith("/")) return "path must be repo-relative (no leading '/')";
|
|
18
|
+
if (path.endsWith("/")) return "path must name a file (no trailing '/')";
|
|
19
|
+
for (const segment of path.split("/")) {
|
|
20
|
+
if (segment.length === 0) return "path must not contain an empty segment";
|
|
21
|
+
if (segment === "." || segment === "..") return "path must not contain '.' or '..' segments";
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function pythonFormEncode(value) {
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
28
|
+
const unreserved = byte >= 48 && byte <= 57 || // 0-9
|
|
29
|
+
byte >= 65 && byte <= 90 || // A-Z
|
|
30
|
+
byte >= 97 && byte <= 122 || // a-z
|
|
31
|
+
byte === 45 || // -
|
|
32
|
+
byte === 46 || // .
|
|
33
|
+
byte === 95 || // _
|
|
34
|
+
byte === 126;
|
|
35
|
+
if (unreserved) {
|
|
36
|
+
out += String.fromCharCode(byte);
|
|
37
|
+
} else if (byte === 32) {
|
|
38
|
+
out += "+";
|
|
39
|
+
} else {
|
|
40
|
+
out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function fileLineRangeAnchorByteLength(anchor) {
|
|
46
|
+
return `anchor=file_line_range&end_line=${anchor.endLine}&path=${pythonFormEncode(anchor.path)}&start_line=${anchor.startLine}`.length;
|
|
47
|
+
}
|
|
48
|
+
function lineNumberProblem(name, value) {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1 || value > FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER) {
|
|
50
|
+
return `${name} must be an integer between 1 and ${FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER}`;
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
function fileLineRangeAnchorProblem(anchor) {
|
|
55
|
+
const problem = repoPathProblem(anchor.path) ?? lineNumberProblem("start_line", anchor.startLine) ?? lineNumberProblem("end_line", anchor.endLine);
|
|
56
|
+
if (problem) return problem;
|
|
57
|
+
if (anchor.endLine < anchor.startLine) return "end_line must be >= start_line";
|
|
58
|
+
const length = fileLineRangeAnchorByteLength(anchor);
|
|
59
|
+
if (length > FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES) {
|
|
60
|
+
return `the file_line_range anchor would be ${length} bytes; at most ${FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES} fit the store — shorten the path`;
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
FILE_LINE_RANGE_BOUNDS,
|
|
66
|
+
fileLineRangeAnchorByteLength,
|
|
67
|
+
fileLineRangeAnchorProblem,
|
|
68
|
+
repoPathProblem
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=file-line-range.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-line-range.js","sources":["../../../../../references/src/anchors/file-line-range.ts"],"sourcesContent":["/**\n * Validation rules for the `file_line_range` anchor — an inclusive, 1-based\n * line range of one file of a repository source (a GitHub `catalog` asset\n * whose files Clio indexes; `docs/clio/21-repository-source.md` §2.1).\n *\n * The rules mirror Clio's `LineRangeLocator` / `validate_repo_path`\n * (`clio/clio/contracts/search.py`) and agora's copy of them\n * (`agora/agora/services/clio/contracts.py`) rule for rule: the same bounds,\n * the same path grammar, and a byte cap measured on the canonical anchor Clio\n * writes — `anchor=file_line_range&end_line=<m>&path=<path>&start_line=<n>`\n * (parameters sorted), form-encoded by Python's `urllib.parse.urlencode`.\n * The builder (`serialize.ts`), the parser (`parse.ts`) and the storage-boundary\n * schema (`schemas.ts`) all read these one rules, so an anchor either fits the\n * store on every side or is refused on every side.\n *\n * Wire contract for what TypeScript emits: `serializeAnchorToParams` sorts the\n * parameters like Clio, but `URLSearchParams` percent-encodes `~` (Clio keeps\n * it) and keeps `*` (Clio writes `%2A`), so for those two characters the two\n * strings differ while decoding to the same anchor. Canonical *string*\n * equality is Clio's own concern — only Clio writes its store, and TS strings\n * never enter it; the TS side guarantees decoded-value equality (both strings\n * parse to one anchor) and a byte cap measured on Clio's encoding\n * (`fileLineRangeAnchorByteLength`).\n */\n\nexport const FILE_LINE_RANGE_BOUNDS = {\n /** Longest repo-relative path, counted in Unicode code points (Python `len`). */\n MAX_REPO_PATH_LENGTH: 1024,\n /** Largest 1-based line number either end of the range may name. */\n MAX_LINE_NUMBER: 10_000_000,\n /** Largest canonical anchor, in UTF-8 bytes, that fits Clio's store. */\n MAX_ANCHOR_BYTES: 2048,\n} as const;\n\n/** The fields of a `file_line_range` anchor (see `FileLineRangeAnchor` in `types.ts`). */\nexport type FileLineRangeFields = { path: string; startLine: number; endLine: number };\n\n/** A UTF-16 surrogate half without its partner — not representable as UTF-8. */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * Why `path` is not a repo-relative, `/`-separated file path, or `undefined`\n * when it is one. Rule for rule the Python `validate_repo_path`: non-empty, at\n * most `MAX_REPO_PATH_LENGTH` code points, valid UTF-8 (no lone surrogate), no\n * NUL, no leading or trailing `/`, no empty, `.` or `..` segment.\n */\nexport function repoPathProblem(path: string): string | undefined {\n if (path.length === 0) return 'path must be non-empty';\n if (Array.from(path).length > FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH) {\n return `path must be at most ${FILE_LINE_RANGE_BOUNDS.MAX_REPO_PATH_LENGTH} characters`;\n }\n if (LONE_SURROGATE.test(path)) return 'path must be valid UTF-8 (no lone surrogates)';\n if (path.includes('\\x00')) return 'path must not contain NUL';\n if (path.startsWith('/')) return \"path must be repo-relative (no leading '/')\";\n if (path.endsWith('/')) return \"path must name a file (no trailing '/')\";\n for (const segment of path.split('/')) {\n if (segment.length === 0) return 'path must not contain an empty segment';\n if (segment === '.' || segment === '..') return \"path must not contain '.' or '..' segments\";\n }\n return undefined;\n}\n\n/**\n * One value as Python's `urllib.parse.urlencode` (`quote_plus`) writes it —\n * the bytes that actually sit in Clio's store. `URLSearchParams` encodes the\n * same way except for `~` (kept here, `%7E` there) and `*` (`%2A` here, kept\n * there), so the byte cap is measured with this encoder, never with\n * `URLSearchParams.toString()`.\n */\nfunction pythonFormEncode(value: string): string {\n let out = '';\n for (const byte of new TextEncoder().encode(value)) {\n const unreserved =\n (byte >= 0x30 && byte <= 0x39) || // 0-9\n (byte >= 0x41 && byte <= 0x5a) || // A-Z\n (byte >= 0x61 && byte <= 0x7a) || // a-z\n byte === 0x2d || // -\n byte === 0x2e || // .\n byte === 0x5f || // _\n byte === 0x7e; // ~\n if (unreserved) {\n out += String.fromCharCode(byte);\n } else if (byte === 0x20) {\n out += '+';\n } else {\n out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n }\n return out;\n}\n\n/**\n * UTF-8 byte length of the canonical anchor Clio writes for these fields\n * (`anchor=file_line_range&end_line=<m>&path=<path>&start_line=<n>`, ASCII).\n * Equals `LineRangeLocator.anchor_length()` on the Python sides.\n */\nexport function fileLineRangeAnchorByteLength(anchor: FileLineRangeFields): number {\n return `anchor=file_line_range&end_line=${anchor.endLine}&path=${pythonFormEncode(anchor.path)}&start_line=${anchor.startLine}`\n .length;\n}\n\nfunction lineNumberProblem(name: string, value: number): string | undefined {\n if (\n !Number.isInteger(value) ||\n value < 1 ||\n value > FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER\n ) {\n return `${name} must be an integer between 1 and ${FILE_LINE_RANGE_BOUNDS.MAX_LINE_NUMBER}`;\n }\n return undefined;\n}\n\n/**\n * Why `anchor` is not a storable `file_line_range` anchor, or `undefined` when\n * it is one. Rule for rule the Python `LineRangeLocator`: the repo-path\n * grammar (`repoPathProblem`), both lines integers in `[1, MAX_LINE_NUMBER]`,\n * `endLine >= startLine`, and the canonical anchor at most `MAX_ANCHOR_BYTES`.\n */\nexport function fileLineRangeAnchorProblem(anchor: FileLineRangeFields): string | undefined {\n const problem =\n repoPathProblem(anchor.path) ??\n lineNumberProblem('start_line', anchor.startLine) ??\n lineNumberProblem('end_line', anchor.endLine);\n if (problem) return problem;\n if (anchor.endLine < anchor.startLine) return 'end_line must be >= start_line';\n const length = fileLineRangeAnchorByteLength(anchor);\n if (length > FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES) {\n return `the file_line_range anchor would be ${length} bytes; at most ${FILE_LINE_RANGE_BOUNDS.MAX_ANCHOR_BYTES} fit the store — shorten the path`;\n }\n return undefined;\n}\n"],"names":[],"mappings":"AAyBO,MAAM,yBAAyB;AAAA;AAAA,EAEpC,sBAAsB;AAAA;AAAA,EAEtB,iBAAiB;AAAA;AAAA,EAEjB,kBAAkB;AACpB;AAMA,MAAM,iBAAiB,WAAA,gFAAA;AAQhB,SAAS,gBAAgB,MAAkC;AAChE,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,MAAM,KAAK,IAAI,EAAE,SAAS,uBAAuB,sBAAsB;AACzE,WAAO,wBAAwB,uBAAuB,oBAAoB;AAAA,EAC5E;AACA,MAAI,eAAe,KAAK,IAAI,EAAG,QAAO;AACtC,MAAI,KAAK,SAAS,IAAM,EAAG,QAAO;AAClC,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AACjC,MAAI,KAAK,SAAS,GAAG,EAAG,QAAO;AAC/B,aAAW,WAAW,KAAK,MAAM,GAAG,GAAG;AACrC,QAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAI,YAAY,OAAO,YAAY,KAAM,QAAO;AAAA,EAClD;AACA,SAAO;AACT;AASA,SAAS,iBAAiB,OAAuB;AAC/C,MAAI,MAAM;AACV,aAAW,QAAQ,IAAI,YAAA,EAAc,OAAO,KAAK,GAAG;AAClD,UAAM,aACH,QAAQ,MAAQ,QAAQ;AAAA,IACxB,QAAQ,MAAQ,QAAQ;AAAA,IACxB,QAAQ,MAAQ,QAAQ;AAAA,IACzB,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AACX,QAAI,YAAY;AACd,aAAO,OAAO,aAAa,IAAI;AAAA,IACjC,WAAW,SAAS,IAAM;AACxB,aAAO;AAAA,IACT,OAAO;AACL,aAAO,IAAI,KAAK,SAAS,EAAE,EAAE,cAAc,SAAS,GAAG,GAAG,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,8BAA8B,QAAqC;AACjF,SAAO,mCAAmC,OAAO,OAAO,SAAS,iBAAiB,OAAO,IAAI,CAAC,eAAe,OAAO,SAAS,GAC1H;AACL;AAEA,SAAS,kBAAkB,MAAc,OAAmC;AAC1E,MACE,CAAC,OAAO,UAAU,KAAK,KACvB,QAAQ,KACR,QAAQ,uBAAuB,iBAC/B;AACA,WAAO,GAAG,IAAI,qCAAqC,uBAAuB,eAAe;AAAA,EAC3F;AACA,SAAO;AACT;AAQO,SAAS,2BAA2B,QAAiD;AAC1F,QAAM,UACJ,gBAAgB,OAAO,IAAI,KAC3B,kBAAkB,cAAc,OAAO,SAAS,KAChD,kBAAkB,YAAY,OAAO,OAAO;AAC9C,MAAI,QAAS,QAAO;AACpB,MAAI,OAAO,UAAU,OAAO,UAAW,QAAO;AAC9C,QAAM,SAAS,8BAA8B,MAAM;AACnD,MAAI,SAAS,uBAAuB,kBAAkB;AACpD,WAAO,uCAAuC,MAAM,mBAAmB,uBAAuB,gBAAgB;AAAA,EAChH;AACA,SAAO;AACT;"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const sheetName = require("../sheet-name.cjs");
|
|
4
|
+
const SHORT_EXCERPT_CHARS = 8;
|
|
5
|
+
const LONG_EXCERPT_CHARS = 20;
|
|
6
|
+
function formatAnchorTime(timestamp) {
|
|
7
|
+
const parts = timestamp.split(":");
|
|
8
|
+
if (parts.length !== 3) return timestamp;
|
|
9
|
+
const [hours, minutes, seconds] = parts.map((part) => Number.parseInt(part, 10));
|
|
10
|
+
if ([hours, minutes, seconds].some((part) => Number.isNaN(part)) || hours < 0 || minutes < 0 || minutes >= 60 || seconds < 0 || seconds >= 60) {
|
|
11
|
+
return timestamp;
|
|
12
|
+
}
|
|
13
|
+
const paddedSeconds = seconds.toString().padStart(2, "0");
|
|
14
|
+
if (hours > 0) return `${hours}:${minutes.toString().padStart(2, "0")}:${paddedSeconds}`;
|
|
15
|
+
return `${minutes}:${paddedSeconds}`;
|
|
16
|
+
}
|
|
17
|
+
function slideName(anchor) {
|
|
18
|
+
return anchor.slideIndex != null ? `Slide ${anchor.slideIndex}` : "Slide";
|
|
19
|
+
}
|
|
20
|
+
function truncate(text, max) {
|
|
21
|
+
if (text.length <= max) return text;
|
|
22
|
+
return `${text.slice(0, max)}…`;
|
|
23
|
+
}
|
|
24
|
+
function formatFileLineRange(anchor, mode) {
|
|
25
|
+
const path = mode === "short" ? anchor.path.split("/").pop() ?? anchor.path : anchor.path;
|
|
26
|
+
const lines = anchor.startLine === anchor.endLine ? `${anchor.startLine}` : `${anchor.startLine}-${anchor.endLine}`;
|
|
27
|
+
return `${path}:${lines}`;
|
|
28
|
+
}
|
|
29
|
+
function formatAnchorShort(anchor) {
|
|
30
|
+
switch (anchor.type) {
|
|
31
|
+
case "cell":
|
|
32
|
+
return `cell ${truncate(anchor.cellId, SHORT_EXCERPT_CHARS)}`;
|
|
33
|
+
case "cell_range":
|
|
34
|
+
return `cells ${truncate(anchor.startCellId, SHORT_EXCERPT_CHARS)} - ${truncate(anchor.endCellId, SHORT_EXCERPT_CHARS)}`;
|
|
35
|
+
case "file_line_range":
|
|
36
|
+
return formatFileLineRange(anchor, "short");
|
|
37
|
+
case "line":
|
|
38
|
+
return `L${anchor.line}`;
|
|
39
|
+
case "line_range":
|
|
40
|
+
return `L${anchor.startLine}-${anchor.endLine}`;
|
|
41
|
+
case "msg":
|
|
42
|
+
return "Msg";
|
|
43
|
+
case "msg_range":
|
|
44
|
+
return "Msgs";
|
|
45
|
+
case "msg_block":
|
|
46
|
+
return `¶${anchor.block}`;
|
|
47
|
+
case "msg_section":
|
|
48
|
+
return truncate(anchor.heading, SHORT_EXCERPT_CHARS);
|
|
49
|
+
case "msg_text":
|
|
50
|
+
return truncate(anchor.text, SHORT_EXCERPT_CHARS);
|
|
51
|
+
case "tool_call":
|
|
52
|
+
return "Tool call";
|
|
53
|
+
case "page":
|
|
54
|
+
return `p.${anchor.page}`;
|
|
55
|
+
case "page_range":
|
|
56
|
+
return `p.${anchor.startPage}-${anchor.endPage}`;
|
|
57
|
+
case "page_rect":
|
|
58
|
+
return `p.${anchor.page} (Region)`;
|
|
59
|
+
case "page_text":
|
|
60
|
+
return `p.${anchor.page}`;
|
|
61
|
+
case "path":
|
|
62
|
+
return anchor.line != null ? `${truncate(anchor.path, SHORT_EXCERPT_CHARS)}:${anchor.line}` : truncate(anchor.path, SHORT_EXCERPT_CHARS);
|
|
63
|
+
case "pm_node":
|
|
64
|
+
case "pm_node_range":
|
|
65
|
+
case "pm_mark":
|
|
66
|
+
return "§";
|
|
67
|
+
case "pixel_range":
|
|
68
|
+
return "Region";
|
|
69
|
+
case "rect":
|
|
70
|
+
return "Region";
|
|
71
|
+
case "shape": {
|
|
72
|
+
const n = anchor.shapeIds.length;
|
|
73
|
+
return n === 1 ? `${slideName(anchor)} • shape` : `${slideName(anchor)} • ${n} shapes`;
|
|
74
|
+
}
|
|
75
|
+
case "sheet_cell":
|
|
76
|
+
return anchor.cell;
|
|
77
|
+
case "sheet_range":
|
|
78
|
+
return anchor.range;
|
|
79
|
+
case "sheet_tab":
|
|
80
|
+
return "Sheet tab";
|
|
81
|
+
case "slide":
|
|
82
|
+
return slideName(anchor);
|
|
83
|
+
case "semantic_query":
|
|
84
|
+
return anchor.field ? `Cube · ${truncate(anchor.field, SHORT_EXCERPT_CHARS)}` : "Cube query";
|
|
85
|
+
case "sql_query":
|
|
86
|
+
return anchor.field ? `SQL · ${truncate(anchor.field, SHORT_EXCERPT_CHARS)}` : "SQL query";
|
|
87
|
+
case "time":
|
|
88
|
+
return formatAnchorTime(anchor.time);
|
|
89
|
+
case "time_range":
|
|
90
|
+
return `${formatAnchorTime(anchor.startTime)}-${formatAnchorTime(anchor.endTime)}`;
|
|
91
|
+
case "comment":
|
|
92
|
+
case "comment_thread":
|
|
93
|
+
return "💬";
|
|
94
|
+
default:
|
|
95
|
+
return "•";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function formatAnchorLong(anchor) {
|
|
99
|
+
switch (anchor.type) {
|
|
100
|
+
case "cell":
|
|
101
|
+
return `Cell ${truncate(anchor.cellId, LONG_EXCERPT_CHARS)}`;
|
|
102
|
+
case "cell_range":
|
|
103
|
+
return `Cells ${truncate(anchor.startCellId, LONG_EXCERPT_CHARS)}–${truncate(anchor.endCellId, LONG_EXCERPT_CHARS)}`;
|
|
104
|
+
case "file_line_range":
|
|
105
|
+
return formatFileLineRange(anchor, "long");
|
|
106
|
+
case "line":
|
|
107
|
+
return `Line ${anchor.line}`;
|
|
108
|
+
case "line_range":
|
|
109
|
+
return `Lines ${anchor.startLine}–${anchor.endLine}`;
|
|
110
|
+
case "msg":
|
|
111
|
+
return "Message";
|
|
112
|
+
case "msg_range":
|
|
113
|
+
return "Messages";
|
|
114
|
+
case "msg_block":
|
|
115
|
+
return `Message block ${anchor.block}`;
|
|
116
|
+
case "msg_section":
|
|
117
|
+
return `Section · ${anchor.heading}`;
|
|
118
|
+
case "msg_text":
|
|
119
|
+
return truncate(anchor.text, LONG_EXCERPT_CHARS);
|
|
120
|
+
case "tool_call":
|
|
121
|
+
return "Tool call";
|
|
122
|
+
case "page":
|
|
123
|
+
return `Page ${anchor.page}`;
|
|
124
|
+
case "page_range":
|
|
125
|
+
return `Pages ${anchor.startPage}–${anchor.endPage}`;
|
|
126
|
+
case "page_rect":
|
|
127
|
+
return `Page ${anchor.page} (Region)`;
|
|
128
|
+
case "page_text":
|
|
129
|
+
return `Page ${anchor.page}`;
|
|
130
|
+
case "path":
|
|
131
|
+
return anchor.line != null ? `${anchor.path}:${anchor.line}${anchor.column != null ? `:${anchor.column}` : ""}` : anchor.path;
|
|
132
|
+
case "pm_node":
|
|
133
|
+
case "pm_node_range":
|
|
134
|
+
case "pm_mark":
|
|
135
|
+
return "§";
|
|
136
|
+
case "pixel_range":
|
|
137
|
+
return "Region";
|
|
138
|
+
case "rect":
|
|
139
|
+
return "Region";
|
|
140
|
+
case "shape": {
|
|
141
|
+
const n = anchor.shapeIds.length;
|
|
142
|
+
return n === 1 ? `${slideName(anchor)} — 1 shape` : `${slideName(anchor)} — ${n} shapes`;
|
|
143
|
+
}
|
|
144
|
+
case "sheet_cell":
|
|
145
|
+
return `Cell ${anchor.cell}`;
|
|
146
|
+
case "sheet_range":
|
|
147
|
+
return `Range ${anchor.range}`;
|
|
148
|
+
case "sheet_tab":
|
|
149
|
+
return "Sheet tab";
|
|
150
|
+
case "slide":
|
|
151
|
+
return slideName(anchor);
|
|
152
|
+
case "semantic_query":
|
|
153
|
+
return anchor.field ? `Cube query · ${truncate(anchor.field, LONG_EXCERPT_CHARS)}` : "Cube query";
|
|
154
|
+
case "sql_query":
|
|
155
|
+
return anchor.field ? `SQL query · ${truncate(anchor.field, LONG_EXCERPT_CHARS)}` : "SQL query";
|
|
156
|
+
case "time":
|
|
157
|
+
return formatAnchorTime(anchor.time);
|
|
158
|
+
case "time_range":
|
|
159
|
+
return `${formatAnchorTime(anchor.startTime)} – ${formatAnchorTime(anchor.endTime)}`;
|
|
160
|
+
case "comment":
|
|
161
|
+
case "comment_thread":
|
|
162
|
+
return "Comment";
|
|
163
|
+
default:
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function isExcerptOnlyAnchor(anchor) {
|
|
168
|
+
return anchor.type === "pm_node" || anchor.type === "pm_node_range" || anchor.type === "pm_mark" || anchor.type === "msg_text";
|
|
169
|
+
}
|
|
170
|
+
function anchorExcerpt({ anchor, meta }) {
|
|
171
|
+
if (anchor.type === "page_text") return anchor.text;
|
|
172
|
+
if (anchor.type === "msg_text") return (meta == null ? void 0 : meta.excerpt) || anchor.text;
|
|
173
|
+
return meta == null ? void 0 : meta.excerpt;
|
|
174
|
+
}
|
|
175
|
+
function formatShort(anchor, meta) {
|
|
176
|
+
const excerpt = anchorExcerpt({ anchor, meta });
|
|
177
|
+
const sheetName$1 = meta == null ? void 0 : meta.sheetName;
|
|
178
|
+
if (excerpt) {
|
|
179
|
+
const anchorPart = formatAnchorShort(anchor);
|
|
180
|
+
if (isExcerptOnlyAnchor(anchor)) {
|
|
181
|
+
return truncate(excerpt, SHORT_EXCERPT_CHARS);
|
|
182
|
+
}
|
|
183
|
+
if (sheetName$1 && (anchor.type === "sheet_cell" || anchor.type === "sheet_range" || anchor.type === "sheet_tab")) {
|
|
184
|
+
return anchor.type === "sheet_tab" ? sheetName$1 : `${sheetName.quoteSheetName(sheetName$1)}!${anchorPart}`;
|
|
185
|
+
}
|
|
186
|
+
return anchorPart;
|
|
187
|
+
}
|
|
188
|
+
if (sheetName$1 && (anchor.type === "sheet_cell" || anchor.type === "sheet_range" || anchor.type === "sheet_tab")) {
|
|
189
|
+
return anchor.type === "sheet_tab" ? sheetName$1 : `${sheetName.quoteSheetName(sheetName$1)}!${formatAnchorShort(anchor)}`;
|
|
190
|
+
}
|
|
191
|
+
return formatAnchorShort(anchor);
|
|
192
|
+
}
|
|
193
|
+
function formatLong(anchor, meta) {
|
|
194
|
+
const excerpt = anchorExcerpt({ anchor, meta });
|
|
195
|
+
const sheetName$1 = meta == null ? void 0 : meta.sheetName;
|
|
196
|
+
if (excerpt) {
|
|
197
|
+
let anchorPart = formatAnchorLong(anchor);
|
|
198
|
+
if (sheetName$1 && (anchor.type === "sheet_cell" || anchor.type === "sheet_range" || anchor.type === "sheet_tab")) {
|
|
199
|
+
anchorPart = anchor.type === "sheet_tab" ? `Sheet tab · ${sheetName$1}` : `${sheetName.quoteSheetName(sheetName$1)}!${anchor.type === "sheet_cell" ? anchor.cell : anchor.range}`;
|
|
200
|
+
}
|
|
201
|
+
if (isExcerptOnlyAnchor(anchor)) {
|
|
202
|
+
return truncate(excerpt, LONG_EXCERPT_CHARS);
|
|
203
|
+
}
|
|
204
|
+
return `${anchorPart}: ${truncate(excerpt, LONG_EXCERPT_CHARS)}`;
|
|
205
|
+
}
|
|
206
|
+
if (sheetName$1 && (anchor.type === "sheet_cell" || anchor.type === "sheet_range" || anchor.type === "sheet_tab")) {
|
|
207
|
+
if (anchor.type === "sheet_tab") {
|
|
208
|
+
return `Sheet tab · ${sheetName$1}`;
|
|
209
|
+
}
|
|
210
|
+
return `${sheetName.quoteSheetName(sheetName$1)}!${anchor.type === "sheet_cell" ? anchor.cell : anchor.range}`;
|
|
211
|
+
}
|
|
212
|
+
return formatAnchorLong(anchor);
|
|
213
|
+
}
|
|
214
|
+
function formatReferenceAnchor(anchor, mode = "long", meta) {
|
|
215
|
+
const text = mode === "short" ? formatShort(anchor, meta) : formatLong(anchor, meta);
|
|
216
|
+
return text === "" ? null : text;
|
|
217
|
+
}
|
|
218
|
+
exports.formatReferenceAnchor = formatReferenceAnchor;
|
|
219
|
+
//# sourceMappingURL=format.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.cjs","sources":["../../../../../references/src/anchors/format.ts"],"sourcesContent":["/**\n * `formatReferenceAnchor` — format an anchor into a human-readable citation line.\n *\n * Accepts the `Anchor` directly (+ optional `AssetMeta` for excerpt / sheetName context).\n * Returns `null` when there is nothing to format. Every renderer of a citation\n * chip labels its anchor with it: Olympus's mention chips and the\n * `@athenaintel/react` SDK's chat chips (through `@athenaintel/references/asset-links`).\n */\n\nimport { quoteSheetName } from '../sheet-name';\nimport type { AssetMeta } from '../types';\nimport type { Anchor, FileLineRangeAnchor, ShapeAnchor, SlideAnchor } from './types';\n\nexport type ReferenceAnchorFormatMode = 'short' | 'long';\n\nconst SHORT_EXCERPT_CHARS = 8;\nconst LONG_EXCERPT_CHARS = 20;\n\n/**\n * A stored `HH:MM:SS` time anchor as a reader sees it (`00:05:30` → `5:30`,\n * `01:05:30` → `1:05:30`); anything else is shown as written.\n */\nfunction formatAnchorTime(timestamp: string): string {\n const parts = timestamp.split(':');\n if (parts.length !== 3) return timestamp;\n const [hours, minutes, seconds] = parts.map((part) => Number.parseInt(part, 10));\n if (\n [hours, minutes, seconds].some((part) => Number.isNaN(part)) ||\n hours < 0 ||\n minutes < 0 ||\n minutes >= 60 ||\n seconds < 0 ||\n seconds >= 60\n ) {\n return timestamp;\n }\n const paddedSeconds = seconds.toString().padStart(2, '0');\n if (hours > 0) return `${hours}:${minutes.toString().padStart(2, '0')}:${paddedSeconds}`;\n return `${minutes}:${paddedSeconds}`;\n}\n\n/** `Slide N` by its 1-based display position; a slide cited by id alone is just `Slide`. */\nfunction slideName(anchor: SlideAnchor | ShapeAnchor): string {\n return anchor.slideIndex != null ? `Slide ${anchor.slideIndex}` : 'Slide';\n}\n\nfunction truncate(text: string, max: number): string {\n if (text.length <= max) return text;\n return `${text.slice(0, max)}…`;\n}\n\n/**\n * `path:startLine-endLine`, or `path:startLine` when the range is one line.\n * The short form names the file by its basename (`indexer.py:10-42`); a path\n * is never head-truncated, which would cut off the part that identifies it.\n */\nfunction formatFileLineRange(anchor: FileLineRangeAnchor, mode: ReferenceAnchorFormatMode): string {\n const path = mode === 'short' ? (anchor.path.split('/').pop() ?? anchor.path) : anchor.path;\n const lines =\n anchor.startLine === anchor.endLine\n ? `${anchor.startLine}`\n : `${anchor.startLine}-${anchor.endLine}`;\n return `${path}:${lines}`;\n}\n\nfunction formatAnchorShort(anchor: Anchor): string {\n switch (anchor.type) {\n case 'cell':\n return `cell ${truncate(anchor.cellId, SHORT_EXCERPT_CHARS)}`;\n case 'cell_range':\n return `cells ${truncate(anchor.startCellId, SHORT_EXCERPT_CHARS)} - ${truncate(anchor.endCellId, SHORT_EXCERPT_CHARS)}`;\n case 'file_line_range':\n return formatFileLineRange(anchor, 'short');\n case 'line':\n return `L${anchor.line}`;\n case 'line_range':\n return `L${anchor.startLine}-${anchor.endLine}`;\n case 'msg':\n return 'Msg';\n case 'msg_range':\n return 'Msgs';\n case 'msg_block':\n return `¶${anchor.block}`;\n case 'msg_section':\n return truncate(anchor.heading, SHORT_EXCERPT_CHARS);\n case 'msg_text':\n return truncate(anchor.text, SHORT_EXCERPT_CHARS);\n case 'tool_call':\n return 'Tool call';\n case 'page':\n return `p.${anchor.page}`;\n case 'page_range':\n return `p.${anchor.startPage}-${anchor.endPage}`;\n case 'page_rect':\n return `p.${anchor.page} (Region)`;\n case 'page_text':\n return `p.${anchor.page}`;\n case 'path':\n return anchor.line != null\n ? `${truncate(anchor.path, SHORT_EXCERPT_CHARS)}:${anchor.line}`\n : truncate(anchor.path, SHORT_EXCERPT_CHARS);\n case 'pm_node':\n case 'pm_node_range':\n case 'pm_mark':\n return '§';\n case 'pixel_range':\n return 'Region';\n case 'rect':\n return 'Region';\n case 'shape': {\n const n = anchor.shapeIds.length;\n return n === 1 ? `${slideName(anchor)} • shape` : `${slideName(anchor)} • ${n} shapes`;\n }\n case 'sheet_cell':\n return anchor.cell;\n case 'sheet_range':\n return anchor.range;\n case 'sheet_tab':\n return 'Sheet tab';\n case 'slide':\n return slideName(anchor);\n case 'semantic_query':\n return anchor.field ? `Cube · ${truncate(anchor.field, SHORT_EXCERPT_CHARS)}` : 'Cube query';\n case 'sql_query':\n return anchor.field ? `SQL · ${truncate(anchor.field, SHORT_EXCERPT_CHARS)}` : 'SQL query';\n case 'time':\n return formatAnchorTime(anchor.time);\n case 'time_range':\n return `${formatAnchorTime(anchor.startTime)}-${formatAnchorTime(anchor.endTime)}`;\n case 'comment':\n case 'comment_thread':\n return '💬';\n default:\n return '•';\n }\n}\n\nfunction formatAnchorLong(anchor: Anchor): string {\n switch (anchor.type) {\n case 'cell':\n return `Cell ${truncate(anchor.cellId, LONG_EXCERPT_CHARS)}`;\n case 'cell_range':\n return `Cells ${truncate(anchor.startCellId, LONG_EXCERPT_CHARS)}–${truncate(anchor.endCellId, LONG_EXCERPT_CHARS)}`;\n case 'file_line_range':\n return formatFileLineRange(anchor, 'long');\n case 'line':\n return `Line ${anchor.line}`;\n case 'line_range':\n return `Lines ${anchor.startLine}–${anchor.endLine}`;\n case 'msg':\n return 'Message';\n case 'msg_range':\n return 'Messages';\n case 'msg_block':\n return `Message block ${anchor.block}`;\n case 'msg_section':\n return `Section · ${anchor.heading}`;\n case 'msg_text':\n return truncate(anchor.text, LONG_EXCERPT_CHARS);\n case 'tool_call':\n return 'Tool call';\n case 'page':\n return `Page ${anchor.page}`;\n case 'page_range':\n return `Pages ${anchor.startPage}–${anchor.endPage}`;\n case 'page_rect':\n return `Page ${anchor.page} (Region)`;\n case 'page_text':\n return `Page ${anchor.page}`;\n case 'path':\n return anchor.line != null\n ? `${anchor.path}:${anchor.line}${anchor.column != null ? `:${anchor.column}` : ''}`\n : anchor.path;\n case 'pm_node':\n case 'pm_node_range':\n case 'pm_mark':\n return '§';\n case 'pixel_range':\n return 'Region';\n case 'rect':\n return 'Region';\n case 'shape': {\n const n = anchor.shapeIds.length;\n return n === 1 ? `${slideName(anchor)} — 1 shape` : `${slideName(anchor)} — ${n} shapes`;\n }\n case 'sheet_cell':\n return `Cell ${anchor.cell}`;\n case 'sheet_range':\n return `Range ${anchor.range}`;\n case 'sheet_tab':\n return 'Sheet tab';\n case 'slide':\n return slideName(anchor);\n case 'semantic_query':\n return anchor.field\n ? `Cube query · ${truncate(anchor.field, LONG_EXCERPT_CHARS)}`\n : 'Cube query';\n case 'sql_query':\n return anchor.field\n ? `SQL query · ${truncate(anchor.field, LONG_EXCERPT_CHARS)}`\n : 'SQL query';\n case 'time':\n return formatAnchorTime(anchor.time);\n case 'time_range':\n return `${formatAnchorTime(anchor.startTime)} – ${formatAnchorTime(anchor.endTime)}`;\n case 'comment':\n case 'comment_thread':\n return 'Comment';\n default:\n return '';\n }\n}\n\n/** Anchors whose excerpt *is* the label — the location alone says nothing to a reader. */\nfunction isExcerptOnlyAnchor(anchor: Anchor): boolean {\n return (\n anchor.type === 'pm_node' ||\n anchor.type === 'pm_node_range' ||\n anchor.type === 'pm_mark' ||\n anchor.type === 'msg_text'\n );\n}\n\nfunction anchorExcerpt({ anchor, meta }: { anchor: Anchor; meta?: AssetMeta }): string | undefined {\n if (anchor.type === 'page_text') return anchor.text;\n if (anchor.type === 'msg_text') return meta?.excerpt || anchor.text;\n return meta?.excerpt;\n}\n\nfunction formatShort(anchor: Anchor, meta?: AssetMeta): string {\n const excerpt = anchorExcerpt({ anchor, meta });\n const sheetName = meta?.sheetName;\n\n if (excerpt) {\n const anchorPart = formatAnchorShort(anchor);\n if (isExcerptOnlyAnchor(anchor)) {\n return truncate(excerpt, SHORT_EXCERPT_CHARS);\n }\n if (\n sheetName &&\n (anchor.type === 'sheet_cell' || anchor.type === 'sheet_range' || anchor.type === 'sheet_tab')\n ) {\n return anchor.type === 'sheet_tab' ? sheetName : `${quoteSheetName(sheetName)}!${anchorPart}`;\n }\n return anchorPart;\n }\n if (\n sheetName &&\n (anchor.type === 'sheet_cell' || anchor.type === 'sheet_range' || anchor.type === 'sheet_tab')\n ) {\n return anchor.type === 'sheet_tab'\n ? sheetName\n : `${quoteSheetName(sheetName)}!${formatAnchorShort(anchor)}`;\n }\n return formatAnchorShort(anchor);\n}\n\nfunction formatLong(anchor: Anchor, meta?: AssetMeta): string {\n const excerpt = anchorExcerpt({ anchor, meta });\n const sheetName = meta?.sheetName;\n\n if (excerpt) {\n let anchorPart = formatAnchorLong(anchor);\n if (\n sheetName &&\n (anchor.type === 'sheet_cell' || anchor.type === 'sheet_range' || anchor.type === 'sheet_tab')\n ) {\n anchorPart =\n anchor.type === 'sheet_tab'\n ? `Sheet tab · ${sheetName}`\n : `${quoteSheetName(sheetName)}!${anchor.type === 'sheet_cell' ? anchor.cell : anchor.range}`;\n }\n if (isExcerptOnlyAnchor(anchor)) {\n return truncate(excerpt, LONG_EXCERPT_CHARS);\n }\n return `${anchorPart}: ${truncate(excerpt, LONG_EXCERPT_CHARS)}`;\n }\n if (\n sheetName &&\n (anchor.type === 'sheet_cell' || anchor.type === 'sheet_range' || anchor.type === 'sheet_tab')\n ) {\n if (anchor.type === 'sheet_tab') {\n return `Sheet tab · ${sheetName}`;\n }\n return `${quoteSheetName(sheetName)}!${anchor.type === 'sheet_cell' ? anchor.cell : anchor.range}`;\n }\n return formatAnchorLong(anchor);\n}\n\n/**\n * Format an anchor into a human-readable citation line.\n *\n * `short` — compact: p.5, L42, 1:30, §, …\n * `long` — expanded: Page 5, Lines 10–25, …\n *\n * Pass `meta` when available for excerpt and sheetName context.\n * Returns `null` only if the formatted string is empty.\n */\nexport function formatReferenceAnchor(\n anchor: Anchor,\n mode: ReferenceAnchorFormatMode = 'long',\n meta?: AssetMeta\n): string | null {\n const text = mode === 'short' ? formatShort(anchor, meta) : formatLong(anchor, meta);\n return text === '' ? null : text;\n}\n"],"names":["sheetName","quoteSheetName"],"mappings":"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"}
|