@athenaintel/react 0.40.3 → 0.41.1

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.
Files changed (82) hide show
  1. package/README.md +59 -0
  2. package/dist/chat/athena-citation-links.cjs +23 -2
  3. package/dist/chat/athena-citation-links.cjs.map +1 -1
  4. package/dist/chat/athena-citation-links.d.ts +5 -0
  5. package/dist/chat/athena-citation-links.js +23 -2
  6. package/dist/chat/athena-citation-links.js.map +1 -1
  7. package/dist/chat-ui/index.d.ts +1 -1
  8. package/dist/chat-ui/lib/asset-urls.cjs +6 -1
  9. package/dist/chat-ui/lib/asset-urls.cjs.map +1 -1
  10. package/dist/chat-ui/lib/asset-urls.d.ts +6 -1
  11. package/dist/chat-ui/lib/asset-urls.js +7 -2
  12. package/dist/chat-ui/lib/asset-urls.js.map +1 -1
  13. package/dist/chat-ui/mentions/mention-chip.cjs +118 -7
  14. package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
  15. package/dist/chat-ui/mentions/mention-chip.d.ts +29 -0
  16. package/dist/chat-ui/mentions/mention-chip.js +120 -9
  17. package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
  18. package/dist/chat-ui/rich-text/chat-message-body.cjs +2 -1
  19. package/dist/chat-ui/rich-text/chat-message-body.cjs.map +1 -1
  20. package/dist/chat-ui/rich-text/chat-message-body.js +2 -1
  21. package/dist/chat-ui/rich-text/chat-message-body.js.map +1 -1
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +2 -2
  25. package/dist/lib/athena-urls.cjs +17 -0
  26. package/dist/lib/athena-urls.cjs.map +1 -1
  27. package/dist/lib/athena-urls.d.ts +14 -0
  28. package/dist/lib/athena-urls.js +17 -0
  29. package/dist/lib/athena-urls.js.map +1 -1
  30. package/dist/react/package.json.cjs +1 -1
  31. package/dist/react/package.json.js +1 -1
  32. package/dist/references/src/anchors/file-line-range.cjs +70 -0
  33. package/dist/references/src/anchors/file-line-range.cjs.map +1 -0
  34. package/dist/references/src/anchors/file-line-range.js +70 -0
  35. package/dist/references/src/anchors/file-line-range.js.map +1 -0
  36. package/dist/references/src/anchors/format.cjs +219 -0
  37. package/dist/references/src/anchors/format.cjs.map +1 -0
  38. package/dist/references/src/anchors/format.js +219 -0
  39. package/dist/references/src/anchors/format.js.map +1 -0
  40. package/dist/references/src/anchors/parse.cjs +408 -0
  41. package/dist/references/src/anchors/parse.cjs.map +1 -0
  42. package/dist/references/src/anchors/parse.js +408 -0
  43. package/dist/references/src/anchors/parse.js.map +1 -0
  44. package/dist/references/src/anchors/serialize.cjs +185 -0
  45. package/dist/references/src/anchors/serialize.cjs.map +1 -0
  46. package/dist/references/src/anchors/serialize.js +185 -0
  47. package/dist/references/src/anchors/serialize.js.map +1 -0
  48. package/dist/references/src/anchors/types.cjs +50 -0
  49. package/dist/references/src/anchors/types.cjs.map +1 -0
  50. package/dist/references/src/anchors/types.js +50 -0
  51. package/dist/references/src/anchors/types.js.map +1 -0
  52. package/dist/references/src/formats/uri/asset-mention-uri.cjs +29 -0
  53. package/dist/references/src/formats/uri/asset-mention-uri.cjs.map +1 -0
  54. package/dist/references/src/formats/uri/asset-mention-uri.js +29 -0
  55. package/dist/references/src/formats/uri/asset-mention-uri.js.map +1 -0
  56. package/dist/references/src/formats/uri/asset-spaces-url.cjs +64 -0
  57. package/dist/references/src/formats/uri/asset-spaces-url.cjs.map +1 -0
  58. package/dist/references/src/formats/uri/asset-spaces-url.js +64 -0
  59. package/dist/references/src/formats/uri/asset-spaces-url.js.map +1 -0
  60. package/dist/references/src/formats/uri/template-placeholder.cjs +9 -0
  61. package/dist/references/src/formats/uri/template-placeholder.cjs.map +1 -0
  62. package/dist/references/src/formats/uri/template-placeholder.js +9 -0
  63. package/dist/references/src/formats/uri/template-placeholder.js.map +1 -0
  64. package/dist/references/src/sheet-name.cjs +10 -0
  65. package/dist/references/src/sheet-name.cjs.map +1 -0
  66. package/dist/references/src/sheet-name.js +10 -0
  67. package/dist/references/src/sheet-name.js.map +1 -0
  68. package/dist/references/src/version-ref.cjs +61 -0
  69. package/dist/references/src/version-ref.cjs.map +1 -0
  70. package/dist/references/src/version-ref.js +61 -0
  71. package/dist/references/src/version-ref.js.map +1 -0
  72. package/dist/styles.css +1 -1
  73. package/dist/workbench/session-workbench-model.cjs +0 -18
  74. package/dist/workbench/session-workbench-model.cjs.map +1 -1
  75. package/dist/workbench/session-workbench-model.d.ts +0 -13
  76. package/dist/workbench/session-workbench-model.js +0 -18
  77. package/dist/workbench/session-workbench-model.js.map +1 -1
  78. package/dist/workbench/session-workbench-panes.cjs +1 -1
  79. package/dist/workbench/session-workbench-panes.cjs.map +1 -1
  80. package/dist/workbench/session-workbench-panes.js +2 -2
  81. package/dist/workbench/session-workbench-panes.js.map +1 -1
  82. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"mention-chip.js","sources":["../../../src/chat-ui/mentions/mention-chip.tsx"],"sourcesContent":["/**\n * Mention chip rendering for message display.\n *\n * Parses [@name](mention://type?...) markdown links and renders them\n * as styled inline chips matching the Olympus mention style.\n * Used in both user messages (inline text) and assistant messages (markdown links).\n */\n\nimport {\n useContext,\n useState,\n type FC,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport {\n formatScreenPoint,\n isScreenPointHref,\n parseScreenPointHref,\n} from \"@athenaintel/agent-runtime\";\nimport { CrosshairIcon, Globe2Icon, UserIcon } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { createOlympusAssetUrl } from \"../lib/asset-urls\";\nimport { MentionAuthCtx } from \"./suggestions/data/api-client\";\nimport { assetTypeToThumb } from \"./suggestions/data/sources/asset-type-icons\";\nimport { useMemberAvatarUrl } from \"./use-member-avatar\";\n\nconst SPACES_MENTION_PATH = /\\/dashboard\\/spaces\\/?\\?/;\n\nfunction isSpacesMentionUrl(url: string): boolean {\n return SPACES_MENTION_PATH.test(url);\n}\n\nfunction getSpacesAssetType(url: URL): string | null {\n const assetType =\n url.searchParams.get(\"assetType\") ||\n url.searchParams.get(\"asset_type\") ||\n url.searchParams.get(\"type\");\n return assetType === \"sheet\" ? \"spreadsheet\" : assetType;\n}\n\n// ── Mention link component for markdown renderer ──\n\n/**\n * Custom <a> renderer for MarkdownTextPrimitive.\n * Intercepts mention:// links and renders them as chips.\n * Falls through to a normal link for non-mention URLs.\n */\nfunction personEmailForMention(\n parsed: ReturnType<typeof parseMentionUrl>,\n): string | undefined {\n if (parsed.type !== \"person\") return undefined;\n return parsed.email || (parsed.id.includes(\"@\") ? parsed.id : undefined);\n}\n\nexport const MentionLink: FC<{\n href?: string;\n children?: ReactNode;\n interactive?: boolean;\n}> = ({ href, children, interactive = true }) => {\n if (\n href &&\n (href.startsWith(\"mention://\") || isSpacesMentionUrl(href))\n ) {\n const name = typeof children === \"string\"\n ? children.replace(/^@/, \"\")\n : Array.isArray(children)\n ? String(children[0] ?? \"\").replace(/^@/, \"\")\n : \"\";\n\n const parsed = parseMentionUrl(href);\n\n return (\n <MentionChip\n name={name || parsed.name}\n type={parsed.type}\n personEmail={personEmailForMention(parsed)}\n assetIds={parsed.assetIds}\n thumbnailUrl={parsed.thumbnailUrl}\n sourceHref={isSpacesMentionUrl(href) ? href : undefined}\n interactive={interactive}\n />\n );\n }\n\n if (href?.startsWith(\"chrome-extension://\")) {\n return <span>{children}</span>;\n }\n\n if (isScreenPointHref(href)) {\n return <ScreenPointChip href={href}>{children}</ScreenPointChip>;\n }\n\n // Normal link — render as-is\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {children}\n </a>\n );\n};\n\n// ── Inline screen-point chip ──\n\n/**\n * Renders an `athena-point://` link as a non-navigating coordinate chip.\n * Surfaces without screenshot context (extension side panel, SDK consumers)\n * get the label plus the coordinates; malformed points fall back to text.\n */\nexport const ScreenPointChip: FC<{\n href: string;\n children?: ReactNode;\n}> = ({ href, children }) => {\n const parsed = parseScreenPointHref(href);\n if (!parsed.ok) {\n return <span>{children}</span>;\n }\n const description = formatScreenPoint(parsed.point);\n return (\n <span\n className=\"not-prose inline-flex max-w-[260px] items-center gap-1 whitespace-nowrap rounded-md border border-slate-300 bg-white px-1.5 py-px align-middle text-[0.85em] font-medium leading-[1.4] text-slate-600\"\n title={description}\n aria-label={description}\n data-screen-point=\"valid\"\n >\n <CrosshairIcon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span className=\"max-w-[180px] overflow-hidden text-ellipsis\">\n {children}\n </span>\n <span className=\"font-normal text-slate-400\">\n {parsed.point.x}, {parsed.point.y}\n </span>\n </span>\n );\n};\n\n// ── Inline mention chip ──\n\nexport const MentionChip: FC<{\n name: string;\n type?: string;\n assetIds?: string[];\n personEmail?: string;\n thumbnailUrl?: string;\n sourceHref?: string;\n interactive?: boolean;\n}> = ({\n name,\n type,\n assetIds,\n personEmail,\n thumbnailUrl,\n sourceHref,\n interactive = true,\n}) => {\n const auth = useContext(MentionAuthCtx);\n const memberAvatarUrl = useMemberAvatarUrl(personEmail);\n const [erroredAvatarUrl, setErroredAvatarUrl] = useState<string>();\n // A citation's excerpt (the passage the chip points at) is the tooltip; the\n // accessible name stays the action.\n const excerpt = sourceHref ? readSourceExcerpt(sourceHref) : null;\n const assetRootHref =\n type === \"asset\" && assetIds?.length\n ? createOlympusAssetUrl({\n appUrl: auth?.appUrl,\n backendUrl: auth?.backendUrl,\n assetIds,\n })\n : null;\n const href = (() => {\n if (!assetRootHref || !sourceHref) return assetRootHref;\n try {\n const source = new URL(sourceHref, \"https://app.athenaintel.com\");\n const destination = new URL(assetRootHref);\n for (const [key, value] of source.searchParams) {\n destination.searchParams.set(key, value);\n }\n destination.searchParams.set(\"asset_ids\", assetIds?.join(\",\") ?? \"\");\n return destination.toString();\n } catch {\n return assetRootHref;\n }\n })();\n // `not-prose` matters: assistant messages render inside Tailwind\n // Typography's `prose`, whose zero-specificity `img` margins (~1.7em\n // top/bottom) would otherwise inflate the chip's icon into a tall card\n // that breaks the sentence flow around it.\n const className = cn(\n \"not-prose inline-flex max-w-[220px] items-center gap-1 whitespace-nowrap rounded-md border border-slate-300 bg-white px-1.5 py-px align-middle text-[0.85em] font-medium leading-[1.4] text-slate-600 no-underline\",\n href &&\n \"cursor-pointer transition-colors hover:border-slate-400 hover:bg-slate-50 hover:text-slate-800 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1\",\n );\n const contents = (\n <>\n {thumbnailUrl && (\n <img\n src={thumbnailUrl}\n alt=\"\"\n className=\"size-3.5 shrink-0\"\n draggable={false}\n />\n )}\n {!thumbnailUrl && type === \"webpage\" && (\n <Globe2Icon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )}\n {!thumbnailUrl && type === \"person\" && (\n memberAvatarUrl && erroredAvatarUrl !== memberAvatarUrl ? (\n <img\n src={memberAvatarUrl}\n alt=\"\"\n className=\"size-3.5 shrink-0 rounded-full object-cover\"\n draggable={false}\n onError={() => setErroredAvatarUrl(memberAvatarUrl)}\n />\n ) : (\n <UserIcon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )\n )}\n <span className=\"max-w-[180px] overflow-hidden text-ellipsis\">\n {name}\n </span>\n </>\n );\n\n if (href && interactive) {\n return (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={className}\n aria-label={`Open ${name} in Spaces`}\n title={excerpt ?? `Open ${name} in Spaces`}\n data-excerpt={excerpt ?? undefined}\n >\n {contents}\n </a>\n );\n }\n\n return (\n <span\n className={className}\n title={excerpt ?? undefined}\n data-excerpt={excerpt ?? undefined}\n >\n {contents}\n </span>\n );\n};\n\n/** The `excerpt` a Spaces citation URL carries, or null. */\nfunction readSourceExcerpt(href: string): string | null {\n try {\n const excerpt = new URL(href, \"https://app.athenaintel.com\").searchParams\n .get(\"excerpt\")\n ?.trim();\n return excerpt ? excerpt : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The label an asset citation chip falls back to when the model wrote none\n * (`[](…/spaces/?asset_ids=…&page=5)`): the cited location, else \"Asset\" — a\n * chip must never render as an empty box.\n */\nfunction citationFallbackLabel(params: URLSearchParams): string {\n const start = params.get(\"start_page\") ?? params.get(\"page\");\n const end = params.get(\"end_page\");\n if (start && end && end !== start) return `Pages ${start}–${end}`;\n if (start) return `Page ${start}`;\n return \"Asset\";\n}\n\n// ── Parse mention:// URLs ──\n\nexport function parseMentionUrl(url: string): {\n type: string;\n name: string;\n id: string;\n email?: string;\n assetIds?: string[];\n thumbnailUrl?: string;\n} {\n try {\n if (isSpacesMentionUrl(url)) {\n const parsed = new URL(url, \"https://app.athenaintel.com\");\n const assetIds = (parsed.searchParams.get(\"asset_ids\") ?? \"\")\n .split(\",\")\n .map((id) => id.trim())\n .filter(Boolean);\n const id = assetIds[0] ?? \"\";\n const assetType = getSpacesAssetType(parsed);\n return {\n type: \"asset\",\n name:\n assetIds.length > 1\n ? `${assetIds.length} assets`\n : citationFallbackLabel(parsed.searchParams),\n id,\n assetIds,\n thumbnailUrl: assetTypeToThumb(assetType),\n };\n }\n // mention://tool?name=...&description=...\n // mention://asset?id=...&assetType=project&thumb=../icons/files/ic-project.svg\n const u = new URL(url);\n const type = u.hostname || u.pathname.replace(/^\\/\\//, \"\").split(\"?\")[0];\n const isWebpage = type === \"webpage\";\n const id = isWebpage\n ? u.searchParams.get(\"tab_id\") || \"\"\n : u.searchParams.get(\"id\") || u.searchParams.get(\"name\") || \"\";\n const name = isWebpage\n ? u.searchParams.get(\"title\") || \"This webpage\"\n : u.searchParams.get(\"name\") || id;\n\n // Get thumbnail: prefer explicit thumb param, then derive from assetType\n let thumbnailUrl = u.searchParams.get(\"thumb\") || undefined;\n if (!thumbnailUrl) {\n const assetType = u.searchParams.get(\"assetType\");\n if (assetType) {\n thumbnailUrl = assetTypeToThumb(assetType);\n } else if (type === \"asset\" || type === \"folder\") {\n thumbnailUrl = assetTypeToThumb(type === \"folder\" ? \"folder\" : null);\n }\n }\n\n return {\n type,\n name,\n id,\n email: type === \"person\" ? u.searchParams.get(\"email\") || undefined : undefined,\n assetIds: type === \"asset\" && id ? [id] : undefined,\n thumbnailUrl,\n };\n } catch {\n return { type: \"unknown\", name: \"\", id: \"\" };\n }\n}\n\nfunction htmlAttribute(attributes: string, name: string): string | null {\n const escapedName = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = new RegExp(\n `(?:^|\\\\s)${escapedName}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`,\n \"i\",\n ).exec(attributes);\n return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;\n}\n\nfunction decodeHtmlAttribute(value: string): string {\n return value\n .replace(/&amp;/gi, \"&\")\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;|&apos;/gi, \"'\")\n .replace(/&lt;/gi, \"<\")\n .replace(/&gt;/gi, \">\");\n}\n\n/** Turn complete model-emitted URL citation anchors into safe Markdown links. */\nexport function normalizeAthenaMentionHtml(text: string): string {\n return text.replace(\n /<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi,\n (original, attributes: string, rawLabel: string) => {\n const className = htmlAttribute(attributes, \"class\") ?? \"\";\n const type = htmlAttribute(attributes, \"data-type\") ?? \"\";\n if (!className.split(/\\s+/).includes(\"mention\") || type !== \"url\") {\n return original;\n }\n\n const rawUrl =\n htmlAttribute(attributes, \"data-url\") ??\n htmlAttribute(attributes, \"href\");\n if (!rawUrl) return original;\n const url = decodeHtmlAttribute(rawUrl);\n let normalizedUrl: string;\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return original;\n }\n normalizedUrl = parsed.toString();\n } catch {\n return original;\n }\n\n const label = decodeHtmlAttribute(rawLabel.replace(/<[^>]*>/g, \"\"))\n .trim()\n .replace(/([\\\\[\\]])/g, \"\\\\$1\");\n return label ? `[${label}](<${normalizedUrl}>)` : original;\n },\n );\n}\n// ── Parse mention markdown in plain text ──\n\n// The label may hold balanced brackets one level deep (`[@Plan [Q3]](…)`) or a lone `[`.\nconst MENTION_RE =\n /\\[@((?:\\[[^[\\]]*\\](?!\\((?:[a-z][a-z0-9+.-]*:\\/\\/|\\/))|[^\\]])*)\\]\\(((?:mention:\\/\\/|(?:https?:\\/\\/[^)\\s]+)?\\/dashboard\\/spaces\\/?\\?)[^)]+)\\)/g;\n\n/**\n * Parse text containing [@name](mention://...) patterns into\n * an array of string segments and MentionChip elements.\n */\nexport function parseMentionsInText(text: string): (string | ReactElement)[] {\n const parts: (string | ReactElement)[] = [];\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n MENTION_RE.lastIndex = 0;\n\n while ((match = MENTION_RE.exec(text)) !== null) {\n if (match.index > lastIndex) {\n parts.push(text.slice(lastIndex, match.index));\n }\n\n const name = match[1];\n const url = match[2];\n const parsed = parseMentionUrl(url);\n\n parts.push(\n <MentionChip\n key={`mention-${match.index}`}\n name={name || parsed.name}\n type={parsed.type}\n personEmail={personEmailForMention(parsed)}\n assetIds={parsed.assetIds}\n thumbnailUrl={parsed.thumbnailUrl}\n sourceHref={isSpacesMentionUrl(url) ? url : undefined}\n />\n );\n\n lastIndex = match.index + match[0].length;\n }\n\n if (lastIndex < text.length) {\n parts.push(text.slice(lastIndex));\n }\n\n return parts.length > 0 ? parts : [text];\n}\n\n/** The text as a reader sees it: each `[@name](…)` mention becomes `@name`. */\nexport function mentionsToPlainText(text: string): string {\n return text.replace(MENTION_RE, (_match, name: string, url: string) => {\n return `@${name || parseMentionUrl(url).name}`;\n });\n}\n"],"names":["id"],"mappings":"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"}
1
+ {"version":3,"file":"mention-chip.js","sources":["../../../src/chat-ui/mentions/mention-chip.tsx"],"sourcesContent":["/**\n * Mention chip rendering for message display.\n *\n * Parses [@name](mention://type?...) markdown links and renders them\n * as styled inline chips matching the Olympus mention style.\n * Used in both user messages (inline text) and assistant messages (markdown links).\n */\n\nimport {\n useContext,\n useState,\n type FC,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport {\n formatScreenPoint,\n isScreenPointHref,\n parseScreenPointHref,\n} from \"@athenaintel/agent-runtime\";\nimport {\n type AssetReference,\n decodeFromAssetMentionUri,\n decodeFromAssetSpacesUrl,\n encodeToAssetSpacesUrl,\n formatReferenceAnchor,\n} from \"@athenaintel/references/asset-links\";\nimport {\n CrosshairIcon,\n DatabaseIcon,\n FileCodeIcon,\n FileIcon,\n FileTextIcon,\n FolderIcon,\n Globe2Icon,\n HeadphonesIcon,\n ImageIcon,\n type LucideIcon,\n MailIcon,\n MessageSquareIcon,\n MonitorIcon,\n PresentationIcon,\n SheetIcon,\n UserIcon,\n VideoIcon,\n} from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { createOlympusAssetUrl } from \"../lib/asset-urls\";\nimport { MentionAuthCtx } from \"./suggestions/data/api-client\";\nimport { assetTypeToThumb } from \"./suggestions/data/sources/asset-type-icons\";\nimport { useMemberAvatarUrl } from \"./use-member-avatar\";\n\nconst SPACES_MENTION_PATH = /\\/dashboard\\/spaces\\/?\\?/;\n\nfunction isSpacesMentionUrl(url: string): boolean {\n return SPACES_MENTION_PATH.test(url);\n}\n\nfunction getSpacesAssetType(url: URL): string | null {\n const assetType =\n url.searchParams.get(\"assetType\") ||\n url.searchParams.get(\"asset_type\") ||\n url.searchParams.get(\"type\");\n return assetType === \"sheet\" ? \"spreadsheet\" : assetType;\n}\n\n/**\n * Where in the asset a citation points, as its chip shows it. The anchor is\n * read and labelled by `@athenaintel/references` — the same parser and labels\n * Olympus's mention chips use — so a chip reads `jwt.py:12-25`, `p.5` or\n * `Slide 3` in every Athena chat.\n */\nexport interface MentionChipCitation {\n /** The anchor type (`page`, `file_line_range`, `slide`, …). */\n anchorType: string;\n /** The short label drawn after the name (`p.5`, `jwt.py:12-25`). */\n label: string;\n /**\n * The place spelled out (`Page 5`, `agora/…/jwt.py:12-25`), without the\n * excerpt: the tooltip when the link carries no excerpt, and part of the\n * chip's accessible name when it holds a word.\n */\n description: string;\n}\n\n/** The citation an asset link's anchor names, or undefined for a plain mention or an anchor nobody can read. */\nfunction citationOf(reference: AssetReference): MentionChipCitation | undefined {\n const { anchor, meta } = reference;\n if (!anchor) return undefined;\n const label = formatReferenceAnchor(anchor, \"short\", meta);\n if (!label) return undefined;\n const { excerpt: _excerpt, ...place } = meta ?? {};\n return {\n anchorType: anchor.type,\n label,\n description: formatReferenceAnchor(anchor, \"long\", place) ?? label,\n };\n}\n\n/**\n * The chip's icon for an asset type when the host serves no asset-type SVGs.\n * A compact set on purpose: the chip renders in every message (and Olympus's\n * main layout reaches it through the queued-message tray), so it carries a\n * dozen icons rather than the `@` menu's whole map.\n */\nconst ASSET_CHIP_ICONS: Record<string, LucideIcon> = {\n document: FileTextIcon,\n word_document: FileTextIcon,\n markdown: FileTextIcon,\n txt: FileTextIcon,\n pdf: FileTextIcon,\n spreadsheet: SheetIcon,\n table: SheetIcon,\n presentation: PresentationIcon,\n powerpoint_deck: PresentationIcon,\n presentation_pptx_studio: PresentationIcon,\n pptx: PresentationIcon,\n image: ImageIcon,\n figure: ImageIcon,\n canvas: ImageIcon,\n video: VideoIcon,\n meeting: VideoIcon,\n video_session: VideoIcon,\n audio: HeadphonesIcon,\n voice_memo: HeadphonesIcon,\n phone_call: HeadphonesIcon,\n email: MailIcon,\n email_message: MailIcon,\n live_email_thread: MailIcon,\n live_calendar_event: MailIcon,\n python: FileCodeIcon,\n javascript: FileCodeIcon,\n typescript: FileCodeIcon,\n json: FileCodeIcon,\n yaml: FileCodeIcon,\n database: DatabaseIcon,\n catalog: DatabaseIcon,\n drive: DatabaseIcon,\n dataset: DatabaseIcon,\n semantic_model: DatabaseIcon,\n query_snippet: DatabaseIcon,\n folder: FolderIcon,\n gdrive_folder: FolderIcon,\n project: FolderIcon,\n collection: FolderIcon,\n session: MessageSquareIcon,\n computer: MonitorIcon,\n desktop_session: MonitorIcon,\n user_interface: MonitorIcon,\n application: MonitorIcon,\n html: Globe2Icon,\n web_page: Globe2Icon,\n browse_session: Globe2Icon,\n};\n\n/** A repository file's lines read as a code file, whatever the catalog holding them. */\nfunction assetChipIcon(\n assetType: string | undefined,\n citation: MentionChipCitation | undefined,\n): LucideIcon {\n if (citation?.anchorType === \"file_line_range\") return FileCodeIcon;\n return (assetType && ASSET_CHIP_ICONS[assetType.toLowerCase()]) || FileIcon;\n}\n\n/** A label with a letter or digit in it; `§` or `•` alone says nothing read aloud. */\nconst SPEAKABLE = /[\\p{L}\\p{N}]/u;\n\n/** The asset reference an asset link decodes to, or null when it does not parse. */\nfunction decodeAssetLink(\n url: URL,\n decode: (url: URL) => AssetReference,\n): AssetReference | null {\n try {\n return decode(url);\n } catch {\n return null;\n }\n}\n\n// ── Mention link component for markdown renderer ──\n\n/**\n * Custom <a> renderer for MarkdownTextPrimitive.\n * Intercepts mention:// links and renders them as chips.\n * Falls through to a normal link for non-mention URLs.\n */\nfunction personEmailForMention(\n parsed: ReturnType<typeof parseMentionUrl>,\n): string | undefined {\n if (parsed.type !== \"person\") return undefined;\n return parsed.email || (parsed.id.includes(\"@\") ? parsed.id : undefined);\n}\n\nexport const MentionLink: FC<{\n href?: string;\n children?: ReactNode;\n interactive?: boolean;\n}> = ({ href, children, interactive = true }) => {\n if (\n href &&\n (href.startsWith(\"mention://\") || isSpacesMentionUrl(href))\n ) {\n const name = typeof children === \"string\"\n ? children.replace(/^@/, \"\")\n : Array.isArray(children)\n ? String(children[0] ?? \"\").replace(/^@/, \"\")\n : \"\";\n\n const parsed = parseMentionUrl(href);\n\n return (\n <MentionChip\n name={name || parsed.name}\n type={parsed.type}\n personEmail={personEmailForMention(parsed)}\n assetIds={parsed.assetIds}\n assetType={parsed.assetType}\n citation={parsed.citation}\n thumbnailUrl={parsed.thumbnailUrl}\n sourceHref={isSpacesMentionUrl(href) ? href : parsed.sourceHref}\n interactive={interactive}\n />\n );\n }\n\n if (href?.startsWith(\"chrome-extension://\")) {\n return <span>{children}</span>;\n }\n\n if (isScreenPointHref(href)) {\n return <ScreenPointChip href={href}>{children}</ScreenPointChip>;\n }\n\n // Normal link — render as-is\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {children}\n </a>\n );\n};\n\n// ── Inline screen-point chip ──\n\n/**\n * Renders an `athena-point://` link as a non-navigating coordinate chip.\n * Surfaces without screenshot context (extension side panel, SDK consumers)\n * get the label plus the coordinates; malformed points fall back to text.\n */\nexport const ScreenPointChip: FC<{\n href: string;\n children?: ReactNode;\n}> = ({ href, children }) => {\n const parsed = parseScreenPointHref(href);\n if (!parsed.ok) {\n return <span>{children}</span>;\n }\n const description = formatScreenPoint(parsed.point);\n return (\n <span\n className=\"not-prose inline-flex max-w-[260px] items-center gap-1 whitespace-nowrap rounded-md border border-slate-300 bg-white px-1.5 py-px align-middle text-[0.85em] font-medium leading-[1.4] text-slate-600\"\n title={description}\n aria-label={description}\n data-screen-point=\"valid\"\n >\n <CrosshairIcon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span className=\"max-w-[180px] overflow-hidden text-ellipsis\">\n {children}\n </span>\n <span className=\"font-normal text-slate-400\">\n {parsed.point.x}, {parsed.point.y}\n </span>\n </span>\n );\n};\n\n// ── Inline mention chip ──\n\nexport const MentionChip: FC<{\n name: string;\n type?: string;\n assetIds?: string[];\n /** The asset's type (`document`, `catalog`, …): picks the icon when the host bundles no SVG thumbnails. */\n assetType?: string;\n /** Where in the asset the link points: its label follows the name. */\n citation?: MentionChipCitation;\n personEmail?: string;\n thumbnailUrl?: string;\n sourceHref?: string;\n interactive?: boolean;\n}> = ({\n name,\n type,\n assetIds,\n assetType,\n citation,\n personEmail,\n thumbnailUrl,\n sourceHref,\n interactive = true,\n}) => {\n const auth = useContext(MentionAuthCtx);\n const memberAvatarUrl = useMemberAvatarUrl(personEmail);\n const [erroredAvatarUrl, setErroredAvatarUrl] = useState<string>();\n // A citation's excerpt (the passage the chip points at) is the tooltip, else\n // the cited place spelled out; the accessible name stays the action.\n const excerpt = sourceHref ? readSourceExcerpt(sourceHref) : null;\n const tooltip = excerpt ?? citation?.description ?? null;\n // A chip the model left unlabelled is already named by its place.\n const anchorLabel =\n citation && name !== citation.description && name !== citation.label\n ? citation.label\n : null;\n const assetRootHref =\n type === \"asset\" && assetIds?.length\n ? createOlympusAssetUrl({\n appUrl: auth?.appUrl,\n backendUrl: auth?.backendUrl,\n assetIds,\n })\n : null;\n const href = (() => {\n if (!assetRootHref || !sourceHref) return assetRootHref;\n try {\n const source = new URL(sourceHref, \"https://app.athenaintel.com\");\n const destination = new URL(assetRootHref);\n for (const [key, value] of source.searchParams) {\n destination.searchParams.set(key, value);\n }\n destination.searchParams.set(\"asset_ids\", assetIds?.join(\",\") ?? \"\");\n return destination.toString();\n } catch {\n return assetRootHref;\n }\n })();\n // `not-prose` matters: assistant messages render inside Tailwind\n // Typography's `prose`, whose zero-specificity `img` margins (~1.7em\n // top/bottom) would otherwise inflate the chip's icon into a tall card\n // that breaks the sentence flow around it.\n const className = cn(\n \"not-prose inline-flex max-w-[220px] items-center gap-1 whitespace-nowrap rounded-md border border-slate-300 bg-white px-1.5 py-px align-middle text-[0.85em] font-medium leading-[1.4] text-slate-600 no-underline\",\n // The cited place gets its own room after a name already at its cap.\n anchorLabel && \"max-w-[340px]\",\n href &&\n \"cursor-pointer transition-colors hover:border-slate-400 hover:bg-slate-50 hover:text-slate-800 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1\",\n );\n const AssetIcon = assetChipIcon(assetType, citation);\n const contents = (\n <>\n {thumbnailUrl && (\n <img\n src={thumbnailUrl}\n alt=\"\"\n className=\"size-3.5 shrink-0\"\n draggable={false}\n />\n )}\n {!thumbnailUrl && (type === \"asset\" || type === \"folder\") && (\n // A host that bundles no asset-type SVGs (computer apps, most SDK\n // apps) still gets the type's icon.\n <AssetIcon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )}\n {!thumbnailUrl && type === \"webpage\" && (\n <Globe2Icon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )}\n {!thumbnailUrl && type === \"person\" && (\n memberAvatarUrl && erroredAvatarUrl !== memberAvatarUrl ? (\n <img\n src={memberAvatarUrl}\n alt=\"\"\n className=\"size-3.5 shrink-0 rounded-full object-cover\"\n draggable={false}\n onError={() => setErroredAvatarUrl(memberAvatarUrl)}\n />\n ) : (\n <UserIcon className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )\n )}\n <span className=\"max-w-[180px] overflow-hidden text-ellipsis\">\n {name}\n </span>\n {citation && anchorLabel && (\n <>\n {/* Copied or read as text, the place stays a word apart from the name; a flex row draws no space. */}{\" \"}\n <span\n className=\"max-w-[140px] shrink-0 overflow-hidden text-ellipsis font-normal text-slate-400\"\n data-citation-anchor={citation.anchorType}\n >\n {anchorLabel}\n </span>\n </>\n )}\n </>\n );\n\n if (href && interactive) {\n return (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={className}\n aria-label={\n anchorLabel && citation && SPEAKABLE.test(citation.description)\n ? `Open ${name} at ${citation.description} in Spaces`\n : `Open ${name} in Spaces`\n }\n title={tooltip ?? `Open ${name} in Spaces`}\n data-excerpt={excerpt ?? undefined}\n // The link's text without its place: what a click names the opened asset.\n data-mention-name={name}\n >\n {contents}\n </a>\n );\n }\n\n return (\n <span\n className={className}\n title={tooltip ?? undefined}\n data-excerpt={excerpt ?? undefined}\n >\n {contents}\n </span>\n );\n};\n\n/** The `excerpt` a Spaces citation URL carries, or null. */\nfunction readSourceExcerpt(href: string): string | null {\n try {\n const excerpt = new URL(href, \"https://app.athenaintel.com\").searchParams\n .get(\"excerpt\")\n ?.trim();\n return excerpt ? excerpt : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The label an asset citation chip falls back to when the model wrote none\n * (`[](…/spaces/?asset_ids=…&page=5)`): the cited location, else \"Asset\" — a\n * chip must never render as an empty box.\n */\nfunction citationFallbackLabel(params: URLSearchParams): string {\n const start = params.get(\"start_page\") ?? params.get(\"page\");\n const end = params.get(\"end_page\");\n if (start && end && end !== start) return `Pages ${start}–${end}`;\n if (start) return `Page ${start}`;\n return \"Asset\";\n}\n\n// ── Parse mention:// URLs ──\n\nexport function parseMentionUrl(url: string): {\n type: string;\n name: string;\n id: string;\n email?: string;\n assetIds?: string[];\n assetType?: string;\n citation?: MentionChipCitation;\n /**\n * For a `mention://asset` citation, the Spaces link to its place (anchor and\n * excerpt), so the chip opens there; a Spaces mention is its own link.\n */\n sourceHref?: string;\n thumbnailUrl?: string;\n} {\n try {\n if (isSpacesMentionUrl(url)) {\n const parsed = new URL(url, \"https://app.athenaintel.com\");\n const assetIds = (parsed.searchParams.get(\"asset_ids\") ?? \"\")\n .split(\",\")\n .map((id) => id.trim())\n .filter(Boolean);\n const id = assetIds[0] ?? \"\";\n const assetType = getSpacesAssetType(parsed);\n // A link to several assets names no one place in any of them.\n const reference =\n assetIds.length === 1\n ? decodeAssetLink(parsed, decodeFromAssetSpacesUrl)\n : null;\n const citation = reference ? citationOf(reference) : undefined;\n return {\n type: \"asset\",\n name:\n assetIds.length > 1\n ? `${assetIds.length} assets`\n : (citation?.description ??\n citationFallbackLabel(parsed.searchParams)),\n id,\n assetIds,\n assetType: assetType ?? undefined,\n citation,\n thumbnailUrl: assetTypeToThumb(assetType),\n };\n }\n // mention://tool?name=...&description=...\n // mention://asset?id=...&assetType=project&thumb=../icons/files/ic-project.svg\n const u = new URL(url);\n const type = u.hostname || u.pathname.replace(/^\\/\\//, \"\").split(\"?\")[0];\n const isWebpage = type === \"webpage\";\n const id = isWebpage\n ? u.searchParams.get(\"tab_id\") || \"\"\n : u.searchParams.get(\"id\") || u.searchParams.get(\"name\") || \"\";\n const name = isWebpage\n ? u.searchParams.get(\"title\") || \"This webpage\"\n : u.searchParams.get(\"name\") || id;\n\n // Get thumbnail: prefer explicit thumb param, then derive from assetType\n let thumbnailUrl = u.searchParams.get(\"thumb\") || undefined;\n if (!thumbnailUrl) {\n const assetType = u.searchParams.get(\"assetType\");\n if (assetType) {\n thumbnailUrl = assetTypeToThumb(assetType);\n } else if (type === \"asset\" || type === \"folder\") {\n thumbnailUrl = assetTypeToThumb(type === \"folder\" ? \"folder\" : null);\n }\n }\n\n const reference =\n type === \"asset\" ? decodeAssetLink(u, decodeFromAssetMentionUri) : null;\n\n return {\n type,\n name,\n id,\n email: type === \"person\" ? u.searchParams.get(\"email\") || undefined : undefined,\n assetIds: type === \"asset\" && id ? [id] : undefined,\n assetType:\n type === \"asset\" || type === \"folder\"\n ? u.searchParams.get(\"assetType\") ||\n reference?.meta?.assetType ||\n (type === \"folder\" ? \"folder\" : undefined)\n : undefined,\n citation: reference ? citationOf(reference) : undefined,\n sourceHref:\n reference && (reference.anchor || reference.meta?.excerpt)\n ? encodeToAssetSpacesUrl(reference)\n : undefined,\n thumbnailUrl,\n };\n } catch {\n return { type: \"unknown\", name: \"\", id: \"\" };\n }\n}\n\nfunction htmlAttribute(attributes: string, name: string): string | null {\n const escapedName = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = new RegExp(\n `(?:^|\\\\s)${escapedName}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`,\n \"i\",\n ).exec(attributes);\n return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;\n}\n\nfunction decodeHtmlAttribute(value: string): string {\n return value\n .replace(/&amp;/gi, \"&\")\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;|&apos;/gi, \"'\")\n .replace(/&lt;/gi, \"<\")\n .replace(/&gt;/gi, \">\");\n}\n\n/** Turn complete model-emitted URL citation anchors into safe Markdown links. */\nexport function normalizeAthenaMentionHtml(text: string): string {\n return text.replace(\n /<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi,\n (original, attributes: string, rawLabel: string) => {\n const className = htmlAttribute(attributes, \"class\") ?? \"\";\n const type = htmlAttribute(attributes, \"data-type\") ?? \"\";\n if (!className.split(/\\s+/).includes(\"mention\") || type !== \"url\") {\n return original;\n }\n\n const rawUrl =\n htmlAttribute(attributes, \"data-url\") ??\n htmlAttribute(attributes, \"href\");\n if (!rawUrl) return original;\n const url = decodeHtmlAttribute(rawUrl);\n let normalizedUrl: string;\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return original;\n }\n normalizedUrl = parsed.toString();\n } catch {\n return original;\n }\n\n const label = decodeHtmlAttribute(rawLabel.replace(/<[^>]*>/g, \"\"))\n .trim()\n .replace(/([\\\\[\\]])/g, \"\\\\$1\");\n return label ? `[${label}](<${normalizedUrl}>)` : original;\n },\n );\n}\n// ── Parse mention markdown in plain text ──\n\n// The label may hold balanced brackets one level deep (`[@Plan [Q3]](…)`) or a lone `[`.\nconst MENTION_RE =\n /\\[@((?:\\[[^[\\]]*\\](?!\\((?:[a-z][a-z0-9+.-]*:\\/\\/|\\/))|[^\\]])*)\\]\\(((?:mention:\\/\\/|(?:https?:\\/\\/[^)\\s]+)?\\/dashboard\\/spaces\\/?\\?)[^)]+)\\)/g;\n\n/**\n * Parse text containing [@name](mention://...) patterns into\n * an array of string segments and MentionChip elements.\n */\nexport function parseMentionsInText(text: string): (string | ReactElement)[] {\n const parts: (string | ReactElement)[] = [];\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n MENTION_RE.lastIndex = 0;\n\n while ((match = MENTION_RE.exec(text)) !== null) {\n if (match.index > lastIndex) {\n parts.push(text.slice(lastIndex, match.index));\n }\n\n const name = match[1];\n const url = match[2];\n const parsed = parseMentionUrl(url);\n\n parts.push(\n <MentionChip\n key={`mention-${match.index}`}\n name={name || parsed.name}\n type={parsed.type}\n personEmail={personEmailForMention(parsed)}\n assetIds={parsed.assetIds}\n assetType={parsed.assetType}\n citation={parsed.citation}\n thumbnailUrl={parsed.thumbnailUrl}\n sourceHref={isSpacesMentionUrl(url) ? url : parsed.sourceHref}\n />\n );\n\n lastIndex = match.index + match[0].length;\n }\n\n if (lastIndex < text.length) {\n parts.push(text.slice(lastIndex));\n }\n\n return parts.length > 0 ? parts : [text];\n}\n\n/** The text as a reader sees it: each `[@name](…)` mention becomes `@name`. */\nexport function mentionsToPlainText(text: string): string {\n return text.replace(MENTION_RE, (_match, name: string, url: string) => {\n return `@${name || parseMentionUrl(url).name}`;\n });\n}\n"],"names":["id","reference"],"mappings":"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"}
@@ -100,7 +100,8 @@ function useChatLinkClick() {
100
100
  }
101
101
  const handled = handleLinkClick({
102
102
  href: anchor.getAttribute("href") ?? anchor.href,
103
- linkText: anchor.textContent,
103
+ // A citation chip's text ends with its place (`p.10`); the asset is named without it.
104
+ linkText: anchor.getAttribute("data-mention-name") ?? anchor.textContent,
104
105
  target: anchor.target,
105
106
  nativeEvent: event.nativeEvent
106
107
  });
@@ -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
- linkText: anchor.textContent,
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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"}
package/dist/index.cjs CHANGED
@@ -330,7 +330,6 @@ exports.SESSION_SANDBOX_LABEL = sessionWorkbenchModel.SESSION_SANDBOX_LABEL;
330
330
  exports.SHELL_OUTPUT_PREVIEW_CHARS = sessionWorkbenchModel.SHELL_OUTPUT_PREVIEW_CHARS;
331
331
  exports.assetPanelTypeOf = sessionWorkbenchModel.assetPanelTypeOf;
332
332
  exports.assetTypeLabel = sessionWorkbenchModel.assetTypeLabel;
333
- exports.athenaLinkAppUrl = sessionWorkbenchModel.athenaLinkAppUrl;
334
333
  exports.computerStateBadge = sessionWorkbenchModel.computerStateBadge;
335
334
  exports.deriveShellRows = sessionWorkbenchModel.deriveShellRows;
336
335
  exports.deriveTaskSteps = sessionWorkbenchModel.deriveTaskSteps;
@@ -472,6 +471,7 @@ exports.useParentAuth = auth.useParentAuth;
472
471
  exports.useParentBridge = auth.useParentBridge;
473
472
  exports.DEFAULT_APP_URL = athenaUrls.DEFAULT_APP_URL;
474
473
  exports.DEFAULT_STATEWIRE_SYNC_URL = athenaUrls.DEFAULT_STATEWIRE_SYNC_URL;
474
+ exports.athenaLinkAppUrl = athenaUrls.athenaLinkAppUrl;
475
475
  exports.createAthenaSpacesUrl = athenaUrls.createAthenaSpacesUrl;
476
476
  exports.deriveAthenaApiUrl = athenaUrls.deriveAthenaApiUrl;
477
477
  exports.deriveAthenaAppUrl = athenaUrls.deriveAthenaAppUrl;
package/dist/index.d.ts CHANGED
@@ -105,7 +105,7 @@ export type { SessionWorkbenchProps } from './workbench/SessionWorkbench';
105
105
  export { SESSION_WORKSPACE_LIVE_REFETCH_MS, useSessionWorkbench, useSessionWorkbenchWorkspace, } from './workbench/session-workbench-context';
106
106
  export type { SessionWorkbenchPane, SessionWorkbenchPaneContext, } from './workbench/session-workbench-context';
107
107
  export { DEFAULT_SESSION_WORKBENCH_OPEN_PANE_IDS, DEFAULT_SESSION_WORKBENCH_PANES, SESSION_WORKBENCH_ATTACHMENTS_PANE, SESSION_WORKBENCH_COMPUTER_PANE, SESSION_WORKBENCH_PULL_REQUESTS_PANE, SESSION_WORKBENCH_SHELL_PANE, SESSION_WORKBENCH_TASKS_PANE, SessionAttachmentsPane, SessionComputerPane, SessionPullRequestsPane, SessionShellPane, SessionTasksPane, } from './workbench/session-workbench-panes';
108
- export { assetPanelTypeOf, assetTypeLabel, athenaLinkAppUrl, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, shellMachine, taskProgress, toolResultText, workspaceComputers, } from './workbench/session-workbench-model';
108
+ export { assetPanelTypeOf, assetTypeLabel, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, shellMachine, taskProgress, toolResultText, workspaceComputers, } from './workbench/session-workbench-model';
109
109
  export type { ComputerStateTone, ShellMachine, ShellRow, ShellRowStatus, TaskProgress, WorkbenchMessage, } from './workbench/session-workbench-model';
110
110
  export { computerSshAccessPath, computerStatusPath, fetchComputerRuntimeStatus, fetchComputerSshAccess, fetchSessionWorkspace, readComputerRuntimeStatus, readComputerSshAccess, readSessionWorkspace, sessionWorkspacePath, } from './workbench/session-workspace-api';
111
111
  export type { ComputerRuntimeStatus, ComputerSshAccess, SessionWorkspace, SessionWorkspaceAsset, SessionWorkspaceRole, } from './workbench/session-workspace-api';
@@ -204,7 +204,7 @@ export { buildStatewireRunConfig } from './runtime/statewire-run-config';
204
204
  export type { StatewireRunConfigOptions } from './runtime/statewire-run-config';
205
205
  export { useParentAuth, useParentBridge } from './runtime/auth';
206
206
  export type { ParentBridge, ParentBridgeState } from './runtime/auth';
207
- export { createAthenaSpacesUrl, DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl, } from './lib/athena-urls';
207
+ export { athenaLinkAppUrl, createAthenaSpacesUrl, DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl, } from './lib/athena-urls';
208
208
  export type { AthenaEnvironment } from './lib/athena-urls';
209
209
  export { Button, buttonVariants } from './ui/button';
210
210
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './ui/tooltip';
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ import { useAssetPanelStore } from "./assets/asset-panel-store.js";
66
66
  import { SESSION_WORKBENCH_ASSETS_PANE_ID, SessionWorkbench } from "./workbench/SessionWorkbench.js";
67
67
  import { SESSION_WORKSPACE_LIVE_REFETCH_MS, useSessionWorkbench, useSessionWorkbenchWorkspace } from "./workbench/session-workbench-context.js";
68
68
  import { DEFAULT_SESSION_WORKBENCH_OPEN_PANE_IDS, DEFAULT_SESSION_WORKBENCH_PANES, SESSION_WORKBENCH_ATTACHMENTS_PANE, SESSION_WORKBENCH_COMPUTER_PANE, SESSION_WORKBENCH_PULL_REQUESTS_PANE, SESSION_WORKBENCH_SHELL_PANE, SESSION_WORKBENCH_TASKS_PANE, SessionAttachmentsPane, SessionComputerPane, SessionPullRequestsPane, SessionShellPane, SessionTasksPane } from "./workbench/session-workbench-panes.js";
69
- import { SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, assetPanelTypeOf, assetTypeLabel, athenaLinkAppUrl, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, shellMachine, taskProgress, toolResultText, workspaceComputers } from "./workbench/session-workbench-model.js";
69
+ import { SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, assetPanelTypeOf, assetTypeLabel, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, shellMachine, taskProgress, toolResultText, workspaceComputers } from "./workbench/session-workbench-model.js";
70
70
  import { computerSshAccessPath, computerStatusPath, fetchComputerRuntimeStatus, fetchComputerSshAccess, fetchSessionWorkspace, readComputerRuntimeStatus, readComputerSshAccess, readSessionWorkspace, sessionWorkspacePath } from "./workbench/session-workspace-api.js";
71
71
  import { useAthenaComputerSshAccess, useAthenaComputerStatus, useAthenaSessionWorkspace } from "./workbench/use-session-workspace.js";
72
72
  import { ComputerLiveDesktop, LIVE_DESKTOP_READY_TIMEOUT_MS } from "./workbench/ComputerLiveDesktop.js";
@@ -112,7 +112,7 @@ import { ComposerQuotePreview } from "./composer/ComposerQuotePreview.js";
112
112
  import { QuoteProvider, createQuoteStore, useQuote } from "./composer/QuoteContext.js";
113
113
  import { AttachmentProvider, createAttachmentStore, useAttachments } from "./composer/AttachmentContext.js";
114
114
  import { useParentAuth, useParentBridge } from "./runtime/auth.js";
115
- import { DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, createAthenaSpacesUrl, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl } from "./lib/athena-urls.js";
115
+ import { DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, athenaLinkAppUrl, createAthenaSpacesUrl, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl } from "./lib/athena-urls.js";
116
116
  import { Button, buttonVariants } from "./ui/button.js";
117
117
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip.js";
118
118
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible.js";
@@ -181,10 +181,27 @@ function createAthenaSpacesUrl({
181
181
  }
182
182
  return url.toString();
183
183
  }
184
+ function originOf(url) {
185
+ try {
186
+ return new URL(url).origin;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+ function athenaLinkAppUrl({
192
+ appUrl,
193
+ backendUrl,
194
+ pageOrigin
195
+ }) {
196
+ const deployed = deriveAthenaAppUrl({ backendUrl });
197
+ if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;
198
+ return appUrl;
199
+ }
184
200
  exports.DEFAULT_APP_URL = DEFAULT_APP_URL;
185
201
  exports.DEFAULT_ATHENA_ENVIRONMENT = DEFAULT_ATHENA_ENVIRONMENT;
186
202
  exports.DEFAULT_STATEWIRE_SYNC_URL = DEFAULT_STATEWIRE_SYNC_URL;
187
203
  exports.assertHostMintedStatewireMount = assertHostMintedStatewireMount;
204
+ exports.athenaLinkAppUrl = athenaLinkAppUrl;
188
205
  exports.createAthenaSpacesUrl = createAthenaSpacesUrl;
189
206
  exports.deriveAthenaApiUrl = deriveAthenaApiUrl;
190
207
  exports.deriveAthenaAppUrl = deriveAthenaAppUrl;
@@ -1 +1 @@
1
- {"version":3,"file":"athena-urls.cjs","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"athena-urls.cjs","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n\nfunction originOf(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * The Athena frontend an \"Open in Athena\" link points at. The provider's\n * `appUrl` is that frontend, except in an app a computer serves: Marathon's\n * `/_athena/auth` mints `appUrl` as the app's OWN origin, so a link built on\n * it opens the app instead of Athena. When `appUrl` is the page's own origin\n * and the Agora base belongs to a known Athena deployment, the deployment's\n * frontend wins (on Olympus itself the two are the same origin anyway). The\n * workbench's \"Open in Athena\" links and the chat's asset chips both go by it.\n */\nexport function athenaLinkAppUrl({\n appUrl,\n backendUrl,\n pageOrigin,\n}: {\n appUrl: string;\n backendUrl: string;\n pageOrigin: string | null;\n}): string {\n const deployed = deriveAthenaAppUrl({ backendUrl });\n if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;\n return appUrl;\n}\n"],"names":[],"mappings":"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"}
@@ -73,3 +73,17 @@ export declare function createAthenaSpacesUrl({ appUrl, assetIds, sessionId, }:
73
73
  assetIds?: string | string[];
74
74
  sessionId?: string;
75
75
  }): string;
76
+ /**
77
+ * The Athena frontend an "Open in Athena" link points at. The provider's
78
+ * `appUrl` is that frontend, except in an app a computer serves: Marathon's
79
+ * `/_athena/auth` mints `appUrl` as the app's OWN origin, so a link built on
80
+ * it opens the app instead of Athena. When `appUrl` is the page's own origin
81
+ * and the Agora base belongs to a known Athena deployment, the deployment's
82
+ * frontend wins (on Olympus itself the two are the same origin anyway). The
83
+ * workbench's "Open in Athena" links and the chat's asset chips both go by it.
84
+ */
85
+ export declare function athenaLinkAppUrl({ appUrl, backendUrl, pageOrigin, }: {
86
+ appUrl: string;
87
+ backendUrl: string;
88
+ pageOrigin: string | null;
89
+ }): string;
@@ -179,11 +179,28 @@ function createAthenaSpacesUrl({
179
179
  }
180
180
  return url.toString();
181
181
  }
182
+ function originOf(url) {
183
+ try {
184
+ return new URL(url).origin;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ function athenaLinkAppUrl({
190
+ appUrl,
191
+ backendUrl,
192
+ pageOrigin
193
+ }) {
194
+ const deployed = deriveAthenaAppUrl({ backendUrl });
195
+ if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;
196
+ return appUrl;
197
+ }
182
198
  export {
183
199
  DEFAULT_APP_URL,
184
200
  DEFAULT_ATHENA_ENVIRONMENT,
185
201
  DEFAULT_STATEWIRE_SYNC_URL,
186
202
  assertHostMintedStatewireMount,
203
+ athenaLinkAppUrl,
187
204
  createAthenaSpacesUrl,
188
205
  deriveAthenaApiUrl,
189
206
  deriveAthenaAppUrl,
@@ -1 +1 @@
1
- {"version":3,"file":"athena-urls.js","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"athena-urls.js","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n\nfunction originOf(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * The Athena frontend an \"Open in Athena\" link points at. The provider's\n * `appUrl` is that frontend, except in an app a computer serves: Marathon's\n * `/_athena/auth` mints `appUrl` as the app's OWN origin, so a link built on\n * it opens the app instead of Athena. When `appUrl` is the page's own origin\n * and the Agora base belongs to a known Athena deployment, the deployment's\n * frontend wins (on Olympus itself the two are the same origin anyway). The\n * workbench's \"Open in Athena\" links and the chat's asset chips both go by it.\n */\nexport function athenaLinkAppUrl({\n appUrl,\n backendUrl,\n pageOrigin,\n}: {\n appUrl: string;\n backendUrl: string;\n pageOrigin: string | null;\n}): string {\n const deployed = deriveAthenaAppUrl({ backendUrl });\n if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;\n return appUrl;\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const version = "0.40.3";
3
+ const version = "0.41.1";
4
4
  const packageJson = {
5
5
  version
6
6
  };