@ai-matrx/kit 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/autosave.cjs +92 -0
  5. package/dist/autosave.cjs.map +1 -0
  6. package/dist/autosave.d.cts +17 -0
  7. package/dist/autosave.d.ts +17 -0
  8. package/dist/autosave.js +72 -0
  9. package/dist/autosave.js.map +1 -0
  10. package/dist/clipboard.cjs +152 -0
  11. package/dist/clipboard.cjs.map +1 -0
  12. package/dist/clipboard.d.cts +17 -0
  13. package/dist/clipboard.d.ts +17 -0
  14. package/dist/clipboard.js +132 -0
  15. package/dist/clipboard.js.map +1 -0
  16. package/dist/color-util.cjs +790 -0
  17. package/dist/color-util.cjs.map +1 -0
  18. package/dist/color-util.d.cts +235 -0
  19. package/dist/color-util.d.ts +235 -0
  20. package/dist/color-util.js +767 -0
  21. package/dist/color-util.js.map +1 -0
  22. package/dist/concurrency.cjs +59 -0
  23. package/dist/concurrency.cjs.map +1 -0
  24. package/dist/concurrency.d.cts +19 -0
  25. package/dist/concurrency.d.ts +19 -0
  26. package/dist/concurrency.js +38 -0
  27. package/dist/concurrency.js.map +1 -0
  28. package/dist/confirm.cjs +344 -0
  29. package/dist/confirm.cjs.map +1 -0
  30. package/dist/confirm.d.cts +132 -0
  31. package/dist/confirm.d.ts +132 -0
  32. package/dist/confirm.js +312 -0
  33. package/dist/confirm.js.map +1 -0
  34. package/dist/delimiter-guard.cjs +172 -0
  35. package/dist/delimiter-guard.cjs.map +1 -0
  36. package/dist/delimiter-guard.d.cts +119 -0
  37. package/dist/delimiter-guard.d.ts +119 -0
  38. package/dist/delimiter-guard.js +151 -0
  39. package/dist/delimiter-guard.js.map +1 -0
  40. package/dist/drafts.cjs +223 -0
  41. package/dist/drafts.cjs.map +1 -0
  42. package/dist/drafts.d.cts +62 -0
  43. package/dist/drafts.d.ts +62 -0
  44. package/dist/drafts.js +201 -0
  45. package/dist/drafts.js.map +1 -0
  46. package/dist/idb-store.cjs +178 -0
  47. package/dist/idb-store.cjs.map +1 -0
  48. package/dist/idb-store.d.cts +111 -0
  49. package/dist/idb-store.d.ts +111 -0
  50. package/dist/idb-store.js +155 -0
  51. package/dist/idb-store.js.map +1 -0
  52. package/dist/idle-scheduler.cjs +268 -0
  53. package/dist/idle-scheduler.cjs.map +1 -0
  54. package/dist/idle-scheduler.d.cts +185 -0
  55. package/dist/idle-scheduler.d.ts +185 -0
  56. package/dist/idle-scheduler.js +246 -0
  57. package/dist/idle-scheduler.js.map +1 -0
  58. package/dist/index.cjs +3113 -0
  59. package/dist/index.cjs.map +1 -0
  60. package/dist/index.d.cts +897 -0
  61. package/dist/index.d.ts +897 -0
  62. package/dist/index.js +3088 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/invalidation.cjs +64 -0
  65. package/dist/invalidation.cjs.map +1 -0
  66. package/dist/invalidation.d.cts +52 -0
  67. package/dist/invalidation.d.ts +52 -0
  68. package/dist/invalidation.js +43 -0
  69. package/dist/invalidation.js.map +1 -0
  70. package/dist/json-format.cjs +319 -0
  71. package/dist/json-format.cjs.map +1 -0
  72. package/dist/json-format.d.cts +148 -0
  73. package/dist/json-format.d.ts +148 -0
  74. package/dist/json-format.js +286 -0
  75. package/dist/json-format.js.map +1 -0
  76. package/dist/latest-request.cjs +36 -0
  77. package/dist/latest-request.cjs.map +1 -0
  78. package/dist/latest-request.d.cts +29 -0
  79. package/dist/latest-request.d.ts +29 -0
  80. package/dist/latest-request.js +16 -0
  81. package/dist/latest-request.js.map +1 -0
  82. package/dist/qr.cjs +109 -0
  83. package/dist/qr.cjs.map +1 -0
  84. package/dist/qr.d.cts +47 -0
  85. package/dist/qr.d.ts +47 -0
  86. package/dist/qr.js +78 -0
  87. package/dist/qr.js.map +1 -0
  88. package/dist/react-tree.cjs +69 -0
  89. package/dist/react-tree.cjs.map +1 -0
  90. package/dist/react-tree.d.cts +38 -0
  91. package/dist/react-tree.d.ts +38 -0
  92. package/dist/react-tree.js +38 -0
  93. package/dist/react-tree.js.map +1 -0
  94. package/dist/search-scoring.cjs +116 -0
  95. package/dist/search-scoring.cjs.map +1 -0
  96. package/dist/search-scoring.d.cts +77 -0
  97. package/dist/search-scoring.d.ts +77 -0
  98. package/dist/search-scoring.js +95 -0
  99. package/dist/search-scoring.js.map +1 -0
  100. package/dist/text-case.cjs +313 -0
  101. package/dist/text-case.cjs.map +1 -0
  102. package/dist/text-case.d.cts +53 -0
  103. package/dist/text-case.d.ts +53 -0
  104. package/dist/text-case.js +292 -0
  105. package/dist/text-case.js.map +1 -0
  106. package/dist/toast.cjs +74 -0
  107. package/dist/toast.cjs.map +1 -0
  108. package/dist/toast.d.cts +92 -0
  109. package/dist/toast.d.ts +92 -0
  110. package/dist/toast.js +54 -0
  111. package/dist/toast.js.map +1 -0
  112. package/dist/url-state.cjs +212 -0
  113. package/dist/url-state.cjs.map +1 -0
  114. package/dist/url-state.d.cts +90 -0
  115. package/dist/url-state.d.ts +90 -0
  116. package/dist/url-state.js +199 -0
  117. package/dist/url-state.js.map +1 -0
  118. package/package.json +286 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/confirm/opener.ts","../src/confirm/host.tsx","../src/confirm/cn.ts","../src/confirm/alert-dialog.tsx","../src/react-tree.ts","../src/confirm/confirm-dialog.tsx"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm — imperative opener.\n *\n * Pure-TS imperative API for the global confirm dialog. Zero React, zero\n * dialog markup — this module is statically importable from anywhere\n * (hooks, utils, Redux thunks, async handlers, sync code, anything).\n *\n * The host (`ConfirmDialogHost`) registers a controller on mount and\n * unregisters on unmount. Calls made before the host has hydrated queue\n * up and resolve as soon as the host is alive — so a destructive action\n * triggered in the first ~50ms after page load still gets a real\n * confirmation, never a silent default-yes/no. With no host ever mounted,\n * a `confirm()` promise stays pending forever (the original's behavior —\n * it never resolves to a silent default).\n *\n * One dialog at a time: concurrent calls queue and present sequentially.\n *\n * Ported verbatim from matrx-frontend\n * `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural\n * inversion: the host/queue state lives on `globalThis` under a\n * `Symbol.for` slot instead of module-level variables. With the package\n * built `splitting: false` in dual ESM/CJS format, this module is\n * duplicated into the root bundle and the `./confirm` bundle, and CJS/ESM\n * each instantiate their own module graph — a module-level variable would\n * silently split the host registration from the callers (the same hazard\n * `@ai-matrx/tap-target` documents for its link registry). Behavior is\n * unchanged; never \"clean this up\" into a module local.\n */\n\nimport type { ReactNode } from \"react\";\n\nexport interface ConfirmOptions {\n title: ReactNode;\n description?: ReactNode | undefined;\n confirmLabel?: string | undefined;\n /** `null` hides the cancel button (acknowledge-only dialogs). */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n}\n\ntype Resolver = (confirmed: boolean) => void;\n\ninterface PendingRequest {\n opts: ConfirmOptions;\n resolve: Resolver;\n}\n\ninterface HostController {\n show: (opts: ConfirmOptions, resolve: Resolver) => void;\n}\n\ninterface OpenerState {\n host: HostController | null;\n queue: PendingRequest[];\n}\n\nconst STATE_SLOT = Symbol.for(\"ai-matrx.kit.confirm-opener-state\");\n\nfunction getState(): OpenerState {\n const holder = globalThis as Record<symbol, OpenerState | undefined>;\n let state = holder[STATE_SLOT];\n if (!state) {\n state = { host: null, queue: [] };\n holder[STATE_SLOT] = state;\n }\n return state;\n}\n\n/** @internal Called by `ConfirmDialogHost` on mount. */\nexport function _registerHost(controller: HostController): void {\n const state = getState();\n state.host = controller;\n while (state.queue.length > 0) {\n const next = state.queue.shift()!;\n controller.show(next.opts, next.resolve);\n }\n}\n\n/** @internal Called by `ConfirmDialogHost` on unmount. */\nexport function _unregisterHost(controller: HostController): void {\n const state = getState();\n if (state.host === controller) state.host = null;\n}\n\n/** @internal Test-only: drop any registered host and pending queue. */\nexport function _resetConfirmOpenerState(): void {\n const state = getState();\n state.host = null;\n state.queue.length = 0;\n}\n\n/**\n * Imperative confirm. Returns a Promise that resolves `true` if the user\n * confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.\n *\n * @example\n * const ok = await confirm({\n * title: \"Delete sandbox\",\n * description: \"This cannot be undone.\",\n * variant: \"destructive\",\n * confirmLabel: \"Delete\",\n * });\n * if (!ok) return;\n */\nexport function confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const state = getState();\n if (state.host) {\n state.host.show(opts, resolve);\n } else {\n state.queue.push({ opts, resolve });\n }\n });\n}\n","\"use client\";\n\n/**\n * `<ConfirmDialogHost />` — render ONCE, near the root of every provider\n * tree, so the imperative `confirm()` always has a live host to dispatch to.\n * Pre-mount calls queue inside `opener.ts` and resolve as soon as the host\n * registers.\n *\n * Ported from matrx-frontend `components/dialogs/confirm/\n * {ConfirmDialogHost,ConfirmDialogHostImpl}.tsx`, with the loading seam\n * inverted: the original split shell + impl and loaded the impl via\n * `next/dynamic({ ssr: false })` to keep radix out of route entry chunks.\n * A framework-agnostic package cannot use `next/dynamic`, so the host is\n * ONE directly-imported component; hosts that want the original's\n * code-splitting lazy-load the subpath themselves, e.g.\n * `dynamic(() => import(\"@ai-matrx/kit/confirm\").then(m => m.ConfirmDialogHost), { ssr: false })`.\n * The \"host renders `<ConfirmDialogHost/>` once\" contract is unchanged.\n *\n * Imperative model: calls to `confirm(...)` from anywhere push a request\n * into a ref-backed queue; this component drains the queue one item at a\n * time and renders a `<ConfirmDialog>` for the currently-active request.\n * Resolving Promise<boolean> happens on Confirm click (true), or on\n * dismiss/cancel (false). The dialog closes immediately on click — callers\n * that need an in-dialog busy spinner during async work should use the\n * inline `<ConfirmDialog>` with the `busy` prop instead.\n */\n\nimport * as React from \"react\";\n\nimport { ConfirmDialog } from \"./confirm-dialog\";\nimport {\n _registerHost,\n _unregisterHost,\n type ConfirmOptions,\n} from \"./opener\";\n\ninterface ActiveRequest {\n opts: ConfirmOptions;\n resolve: (confirmed: boolean) => void;\n}\n\nexport function ConfirmDialogHost() {\n const [active, setActive] = React.useState<ActiveRequest | null>(null);\n const [tick, setTick] = React.useState(0);\n const queueRef = React.useRef<ActiveRequest[]>([]);\n\n // Register/unregister the controller exactly once. The controller's\n // `show` always pushes onto the queue and bumps `tick`; the drain\n // effect below picks up from there. This avoids stale-closure bugs\n // around `active`.\n React.useEffect(() => {\n const controller = {\n show: (opts: ConfirmOptions, resolve: (confirmed: boolean) => void) => {\n queueRef.current.push({ opts, resolve });\n setTick((n) => n + 1);\n },\n };\n _registerHost(controller);\n return () => _unregisterHost(controller);\n }, []);\n\n // Drain the queue whenever nothing is showing.\n React.useEffect(() => {\n if (active === null && queueRef.current.length > 0) {\n setActive(queueRef.current.shift()!);\n }\n }, [active, tick]);\n\n const handleConfirm = React.useCallback(() => {\n if (!active) return;\n active.resolve(true);\n setActive(null);\n }, [active]);\n\n const handleOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open && active) {\n active.resolve(false);\n setActive(null);\n }\n },\n [active],\n );\n\n return (\n <ConfirmDialog\n open={!!active}\n onOpenChange={handleOpenChange}\n title={active?.opts.title ?? \"\"}\n description={active?.opts.description}\n confirmLabel={active?.opts.confirmLabel}\n cancelLabel={active?.opts.cancelLabel}\n variant={active?.opts.variant}\n onConfirm={handleConfirm}\n />\n );\n}\n","import { twMerge } from \"tailwind-merge\";\n\n/**\n * Tailwind-aware className merge. The original app's `cn` is\n * `twMerge(clsx(inputs))`; here `clsx` is dropped (every call site passes\n * strings / false), but `tailwind-merge` is KEPT on purpose: the public\n * `className` / `contentClassName` overrides depend on last-wins conflict\n * resolution (e.g. a host's `max-w-3xl` must beat the built-in `max-w-lg`,\n * a destructive `bg-destructive` must beat the default `bg-primary`).\n * A naive join would leave both classes applied and let stylesheet order\n * decide — a real behavior divergence from the original.\n */\nexport function cn(\n ...values: Array<string | null | undefined | false>\n): string {\n return twMerge(values.filter(Boolean).join(\" \"));\n}\n","\"use client\";\n\n/**\n * Inlined shadcn-style wrapper over `@radix-ui/react-alert-dialog` — the one\n * real runtime dependency of the `./confirm` subpath (this subpath's product\n * IS the dialog). Ported from matrx-frontend `components/ui/alert-dialog.tsx`\n * with the host-shaped seams inverted:\n *\n * - `usePopoutContainer` (window-panels popout portal retargeting) is dropped:\n * the portal targets the Radix default (`document.body`). Hosts with exotic\n * portal needs pass `container` on `AlertDialogPortal` themselves.\n * - `buttonVariants` from the design system is inlined as the exact class\n * strings the two footer buttons use (base + default + outline variants,\n * design-system `button.tsx` as of this port). No cva dependency.\n * - Styling keeps the Tailwind semantic-token classes VERBATIM (`bg-background`,\n * `text-muted-foreground`, `bg-primary`, `border-border`, ...) — the platform\n * vocabulary. Hosts on other design systems override via the `className` /\n * `contentClassName` props (classes merge last-wins via tailwind-merge).\n *\n * THE ROOT RENDERS UNCONDITIONALLY — no mount gate. Radix ids come from\n * React's SSR-stable `useId`, so there is no SSR/client id mismatch to hide\n * from (the original's D144 ruling).\n */\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"./cn\";\nimport { treeContainsComponent } from \"../react-tree\";\n\n/**\n * Inlined design-system button classes (base + the two variants the alert\n * dialog footer uses). Source of truth while the originals live:\n * aidream `apps/shared/design-system/src/button.tsx`.\n */\nconst buttonBase =\n \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 h-9 px-4 py-2\";\nconst buttonDefault =\n \"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90\";\nconst buttonOutline =\n \"border border-border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-[10000] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n ref={ref}\n />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\n/**\n * Unstyled, non-portalling Content for custom AlertDialog layouts. AlertDialog\n * is always modal, so this keeps its ARIA semantics explicit and consistent.\n */\nconst AlertDialogContentPrimitive = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ ...props }, ref) => (\n <AlertDialogPrimitive.Content {...props} ref={ref} aria-modal=\"true\" />\n));\nAlertDialogContentPrimitive.displayName = \"AlertDialogContentPrimitive\";\n\nconst AlertDialogDescription = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nAlertDialogDescription.displayName =\n AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => {\n const hasDescription =\n treeContainsComponent(children, AlertDialogDescription) ||\n treeContainsComponent(children, AlertDialogPrimitive.Description);\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogContentPrimitive\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-[10000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n className,\n )}\n {...props}\n >\n {!hasDescription && (\n <AlertDialogPrimitive.Description className=\"sr-only\">\n Please confirm the action described in this dialog.\n </AlertDialogPrimitive.Description>\n )}\n {children}\n </AlertDialogContentPrimitive>\n </AlertDialogPortal>\n );\n});\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-2 text-center sm:text-left\",\n className,\n )}\n {...props}\n />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className,\n )}\n {...props}\n />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Title\n ref={ref}\n className={cn(\"text-lg font-semibold\", className)}\n {...props}\n />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Action>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Action\n ref={ref}\n className={cn(buttonBase, buttonDefault, className)}\n {...props}\n />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Cancel\n ref={ref}\n className={cn(buttonBase, buttonOutline, \"mt-2 sm:mt-0\", className)}\n {...props}\n />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogContentPrimitive,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n};\n","/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\"\n ) {\n const keys =\n typeof node === \"object\"\n ? ` with keys {${Object.keys(node).join(\", \")}}`\n : \"\";\n console.error(\n `[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. ` +\n \"React will throw 'Objects are not valid as a React child' at the real render site. \" +\n \"Stringify it (e.g. JSON.stringify) before rendering.\",\n node,\n );\n }\n return false;\n}\n","\"use client\";\n\n/**\n * Declarative `<ConfirmDialog />` — drop-in replacement for `window.confirm`.\n * Ported verbatim from matrx-frontend `components/ui/confirm-dialog.tsx`;\n * the only inversion is the busy spinner: `Loader2` from lucide-react is\n * inlined as a single SVG (the `@ai-matrx/tap-target` precedent — one icon\n * does not justify an icon dependency). Path annotated below.\n *\n * Pattern: hold the pending target in state, render <ConfirmDialog />\n * once at the bottom of the component, and open it by setting the target.\n * When busy state is meaningful (e.g. a network delete that should hold the\n * dialog open with a spinner), use THIS component inline; the imperative\n * `confirm()` closes immediately on click.\n */\n\nimport * as React from \"react\";\n\nimport { cn } from \"./cn\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"./alert-dialog\";\n\n/** lucide `loader-circle` (a.k.a. `Loader2`) v1.22.0, inlined. */\nfunction SpinnerIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={24}\n height={24}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className={className}\n >\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n );\n}\n\nexport interface ConfirmDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: React.ReactNode;\n description?: React.ReactNode | undefined;\n /**\n * Rich body rendered between the header and the footer, OUTSIDE the\n * description `<p>` — use for block-level content (diffs, previews, lists)\n * that would be invalid HTML inside `description`.\n */\n content?: React.ReactNode | undefined;\n /** Extra classes for the dialog content (e.g. a wider max-w for diffs). */\n contentClassName?: string | undefined;\n confirmLabel?: string | undefined;\n /**\n * `null` hides the cancel button entirely — for acknowledge-only dialogs\n * where there is nothing to cancel. Anything else labels it.\n */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n busy?: boolean | undefined;\n /**\n * Blocks confirming without pretending work is in flight. For a dialog whose\n * `content` asks the user something the action cannot proceed without — the\n * choice is missing, not loading — `busy` would show a misleading spinner.\n */\n confirmDisabled?: boolean | undefined;\n onConfirm: () => void | Promise<void>;\n}\n\n/**\n * Drop-in replacement for `window.confirm`. Use this anywhere you would\n * otherwise reach for a browser-level confirm dialog.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n content,\n contentClassName,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n variant = \"default\",\n busy = false,\n confirmDisabled = false,\n onConfirm,\n}: ConfirmDialogProps) {\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className={contentClassName}>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n {description ? (\n <AlertDialogDescription>{description}</AlertDialogDescription>\n ) : null}\n </AlertDialogHeader>\n {content ?? null}\n <AlertDialogFooter>\n {cancelLabel === null ? null : (\n <AlertDialogCancel className=\"max-lg:min-h-11\" disabled={busy}>\n {cancelLabel}\n </AlertDialogCancel>\n )}\n <AlertDialogAction\n disabled={busy || confirmDisabled}\n onClick={(event) => {\n event.preventDefault();\n void onConfirm();\n }}\n className={cn(\n \"max-lg:min-h-11\",\n variant === \"destructive\" &&\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n )}\n >\n {busy ? <SpinnerIcon className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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","names":["React","React","jsx","jsxs","jsx"]}
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/delimiter-guard.ts
21
+ var delimiter_guard_exports = {};
22
+ __export(delimiter_guard_exports, {
23
+ guardMarkdownDelimiters: () => guardMarkdownDelimiters,
24
+ guardMathDelimiters: () => guardMathDelimiters,
25
+ guardRunawayLinks: () => guardRunawayLinks,
26
+ reportDelimiterViolations: () => reportDelimiterViolations
27
+ });
28
+ module.exports = __toCommonJS(delimiter_guard_exports);
29
+ var ZWSP = "\u200B";
30
+ var ESCAPED_DOLLARS = `${ZWSP}$${ZWSP}$`;
31
+ var ESCAPED_BRACKET = `${ZWSP}&#91;`;
32
+ var MAX_MATH_SPAN = 600;
33
+ var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(?:^|\n)[ \t]*[-*+][ \t]+|(?:^|\n)[ \t]*\d+[.)][ \t]/;
34
+ var LATEX_COMMAND = /\\[a-zA-Z]/;
35
+ var PROSE_WORD = /[A-Za-z]{3,}/g;
36
+ var PROSE_WORD_LIMIT = 6;
37
+ function looksLikeMath(inner) {
38
+ const s = inner.trim();
39
+ if (!s) return false;
40
+ if (STRUCTURAL_MARKDOWN.test(s)) return false;
41
+ if (s.length > MAX_MATH_SPAN) return false;
42
+ if (LATEX_COMMAND.test(s)) {
43
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;
44
+ }
45
+ if (/\n[ \t]*\n/.test(s)) return false;
46
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
47
+ }
48
+ function protectedRanges(text) {
49
+ const ranges = [];
50
+ const patterns = [/```[\s\S]*?(?:```|$)/g, /~~~[\s\S]*?(?:~~~|$)/g, /`[^`\n]*`/g];
51
+ for (const re of patterns) {
52
+ let m;
53
+ while ((m = re.exec(text)) !== null) {
54
+ ranges.push([m.index, m.index + m[0].length]);
55
+ }
56
+ }
57
+ return ranges;
58
+ }
59
+ function isProtected(index, ranges) {
60
+ return ranges.some(([start, end]) => index >= start && index < end);
61
+ }
62
+ function preview(text, max = 160) {
63
+ const flat = text.replace(/\s+/g, " ").trim();
64
+ return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
65
+ }
66
+ function guardMathDelimiters(text) {
67
+ if (!text.includes("$$")) return { text, violations: [] };
68
+ const ranges = protectedRanges(text);
69
+ const tokens = [];
70
+ for (let i = 0; i < text.length - 1; i++) {
71
+ if (text[i] !== "$" || text[i + 1] !== "$") continue;
72
+ if (!isProtected(i, ranges)) tokens.push(i);
73
+ i++;
74
+ }
75
+ if (tokens.length === 0) return { text, violations: [] };
76
+ const violations = [];
77
+ const escapeAt = [];
78
+ let j = 0;
79
+ while (j < tokens.length) {
80
+ const open = tokens[j];
81
+ const close = tokens[j + 1];
82
+ if (close === void 0) {
83
+ violations.push({
84
+ reason: "unpaired",
85
+ index: open,
86
+ spanLength: 0,
87
+ // An unpaired `$$` is inert to remark-math (nothing closes it), so it
88
+ // is reported but NOT escaped — it already renders as literal text.
89
+ preview: preview(text.slice(open, open + 120))
90
+ });
91
+ break;
92
+ }
93
+ const inner = text.slice(open + 2, close);
94
+ if (looksLikeMath(inner)) {
95
+ j += 2;
96
+ continue;
97
+ }
98
+ escapeAt.push(open);
99
+ violations.push({
100
+ reason: "prose-span",
101
+ index: open,
102
+ spanLength: inner.length,
103
+ preview: preview(inner)
104
+ });
105
+ j += 1;
106
+ }
107
+ let guarded = text;
108
+ for (const index of [...escapeAt].sort((a, b) => b - a)) {
109
+ guarded = `${guarded.slice(0, index)}${ESCAPED_DOLLARS}${guarded.slice(index + 2)}`;
110
+ }
111
+ return { text: guarded, violations };
112
+ }
113
+ var MAX_LINK_LABEL = 200;
114
+ var LABEL_BLOCK_STRUCTURE = /\n[ \t]*\n|(?:^|\n)[ \t]*[-*+][ \t]+|#{2,6}[ \t]/;
115
+ function guardRunawayLinks(text) {
116
+ if (!text.includes("[")) return { text, violations: [] };
117
+ const ranges = protectedRanges(text);
118
+ const violations = [];
119
+ const escapeAt = [];
120
+ const linkRe = /\[((?:[^[\]]|\\.)*)\]\(([^\s)]*)/g;
121
+ let m;
122
+ while ((m = linkRe.exec(text)) !== null) {
123
+ const open = m.index;
124
+ if (isProtected(open, ranges)) continue;
125
+ const label = m[1] ?? "";
126
+ const runaway = label.length > MAX_LINK_LABEL || LABEL_BLOCK_STRUCTURE.test(label);
127
+ if (!runaway) continue;
128
+ escapeAt.push(open);
129
+ violations.push({
130
+ reason: "runaway-link",
131
+ index: open,
132
+ spanLength: label.length,
133
+ preview: preview(label)
134
+ });
135
+ }
136
+ let guarded = text;
137
+ for (const index of [...escapeAt].sort((a, b) => b - a)) {
138
+ guarded = `${guarded.slice(0, index)}${ESCAPED_BRACKET}${guarded.slice(index + 1)}`;
139
+ }
140
+ return { text: guarded, violations };
141
+ }
142
+ function guardMarkdownDelimiters(text) {
143
+ const math = guardMathDelimiters(text);
144
+ const links = guardRunawayLinks(math.text);
145
+ return {
146
+ text: links.text,
147
+ violations: [...math.violations, ...links.violations]
148
+ };
149
+ }
150
+ function reportDelimiterViolations(violations, context) {
151
+ if (violations.length === 0) return;
152
+ try {
153
+ const worst = violations.find((v) => v.reason !== "unpaired") ?? violations[0];
154
+ if (!worst) return;
155
+ const message = worst.reason === "prose-span" ? `Malformed math delimiters: a stray "$$" would have turned ${worst.spanLength} chars of prose into a math span (KaTeX would render it as red error text). Escaped it.` : worst.reason === "runaway-link" ? `Runaway markdown link: an unclosed "[" would have turned ${worst.spanLength} chars into one link label. Escaped it.` : `Malformed math delimiters: an unpaired "$$" reached the renderer.`;
156
+ console.warn(`[markdown-delimiter-guard] ${message}`, {
157
+ renderPath: context.renderPath,
158
+ violations
159
+ });
160
+ context.capture?.({
161
+ source: "markdown-delimiters",
162
+ message,
163
+ relation: `markdown:${context.renderPath}`,
164
+ details: worst.preview,
165
+ conversationId: context.conversationId,
166
+ callSite: "guardMarkdownDelimiters",
167
+ raw: { messageId: context.messageId, violations }
168
+ });
169
+ } catch {
170
+ }
171
+ }
172
+ //# sourceMappingURL=delimiter-guard.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/delimiter-guard.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/delimiter-guard — stops ONE stray markdown delimiter from\n * swallowing a whole section of a streamed answer.\n *\n * THE FAILURE CLASS\n * -----------------\n * Markdown delimiters pair greedily and blindly. A single stray opener emitted\n * by a model (the common shape is a mangled citation:\n * `…/a-quicker-way-to-heal-prp-and-prf$$ .`) pairs with the next matching\n * delimiter anywhere later in the message, and everything in between —\n * headings, bold, links, whole sections — collapses into one node.\n *\n * Two delimiters cause this in a remark-math + CommonMark pipeline:\n *\n * 1. `$$` (remark-math). The swallowed prose becomes a math node, KaTeX fails\n * to parse it, and `rehype-katex` falls back to its built-in error\n * rendering: the raw source re-emitted inside `<span class=\"katex-error\"\n * style=\"color:#cc0000\">`. The symptom is a huge block of BRIGHT RED\n * unrendered markdown mid-answer. That red is KaTeX reporting a parse error\n * on text that was never math — not a style of yours.\n * 2. `[` (CommonMark link label). The swallowed prose becomes the label of one\n * enormous hyperlink — the same bug wearing blue instead of red.\n *\n * WHAT THIS DOES\n * --------------\n * Before the markdown pipeline runs, each candidate span is checked for\n * plausibility. A `$$…$$` span carrying markdown structure (links, URLs, bold,\n * headings, list markers) or reading as prose is not math; a link label that is\n * hundreds of characters long or contains block structure is not a label. The\n * offending OPENER is neutralized (`&#36;&#36;`, `&#91;`) and scanning resumes at the next\n * delimiter, so genuine math and genuine links later in the same message still\n * render. Real content is never touched.\n *\n * LOUD RECOVERY: every firing is a real upstream defect (a model emitting\n * malformed delimiters, or a producer mangling a citation). Callers report the\n * returned violations — see `reportDelimiterViolations`.\n *\n * Ported verbatim from matrx-frontend `lib/markdown/delimiter-guard.ts`, with\n * ONE coupling inversion: the app's `captureError` store import became the\n * injected `capture` sink on `reportDelimiterViolations`' context (payload\n * shape preserved exactly). No sink means console-only loud recovery.\n */\n\nexport type DelimiterViolationReason =\n /** A `$$…$$` pair whose contents are prose/markdown, not math. */\n | \"prose-span\"\n /** A `$$` with no closing partner in the content. */\n | \"unpaired\"\n /** A `[…](…)` link whose label swallowed prose/structure. */\n | \"runaway-link\";\n\nexport interface DelimiterViolation {\n reason: DelimiterViolationReason;\n /** Character offset of the offending delimiter in the input string. */\n index: number;\n /** Length of the span between the delimiters (0 for `unpaired`). */\n spanLength: number;\n /** Short excerpt of what would have been swallowed. */\n preview: string;\n}\n\nexport interface DelimiterGuardResult {\n /** Input with runaway openers escaped. */\n text: string;\n violations: DelimiterViolation[];\n}\n\n/**\n * Neutralized delimiters. Two requirements drove this encoding:\n *\n * - It must be INVISIBLE in the rendered output. A backslash escape (`\\$\\$`)\n * is emitted literally when the delimiter abuts constructs remark does not\n * re-parse, so the reader sees stray backslashes.\n * - It must not be swallowed by the GFM autolink extension. These strays sit\n * right after a bare URL (that is how they are produced), and a character\n * reference placed there is absorbed into the link target instead of being\n * decoded.\n *\n * A zero-width space satisfies both: it terminates the autolink, splits the\n * `$$` token so remark-math never sees a delimiter (single `$` is inert —\n * `singleDollarTextMath: false`), and renders as nothing. The bracket keeps a\n * character reference (a lone `[` has no token to split) behind a ZWSP.\n */\nconst ZWSP = \"\\u200B\";\nconst ESCAPED_DOLLARS = `${ZWSP}$${ZWSP}$`;\nconst ESCAPED_BRACKET = `${ZWSP}&#91;`;\n\n/** Longest span we will accept as real math when no LaTeX command is present. */\nconst MAX_MATH_SPAN = 600;\n\n/**\n * Markdown structure that can never appear inside real math:\n * a markdown link, a bare URL, bold markers, an ATX heading, or a\n * line-leading list marker.\n */\nconst STRUCTURAL_MARKDOWN =\n /\\]\\(|https?:\\/\\/|\\*\\*|(?:^|\\n)[ \\t]{0,3}#{1,6}[ \\t]|(?:^|\\n)[ \\t]*[-*+][ \\t]+|(?:^|\\n)[ \\t]*\\d+[.)][ \\t]/;\n\n/** A LaTeX control sequence (`\\frac`, `\\sim`, `\\text`, …). */\nconst LATEX_COMMAND = /\\\\[a-zA-Z]/;\n\n/** Alphabetic words of 3+ letters — the prose signal for command-free spans. */\nconst PROSE_WORD = /[A-Za-z]{3,}/g;\n\n/** Word count at which a LaTeX-command-free span is judged to be prose. */\nconst PROSE_WORD_LIMIT = 6;\n\nfunction looksLikeMath(inner: string): boolean {\n const s = inner.trim();\n if (!s) return false;\n\n // Structural markdown wins over every other signal — a swallowed prose span\n // routinely contains real LaTeX fragments (`$\\sim 200 \\text{ g}$`) picked up\n // from the sentences it ate, so the command check cannot run first.\n if (STRUCTURAL_MARKDOWN.test(s)) return false;\n\n if (s.length > MAX_MATH_SPAN) return false;\n\n // A LaTeX control sequence is strong evidence of math — but not proof: a\n // swallowed span often eats sentences that themselves contained inline math\n // (`$\\sim 400 \\text{ g}$`). Real math is symbol-dense, so a span that is\n // mostly English words is still prose.\n if (LATEX_COMMAND.test(s)) {\n return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;\n }\n\n if (/\\n[ \\t]*\\n/.test(s)) return false;\n\n return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;\n}\n\n/** Ranges (fenced blocks, inline code) whose `$$` must be ignored. */\nfunction protectedRanges(text: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n const patterns = [/```[\\s\\S]*?(?:```|$)/g, /~~~[\\s\\S]*?(?:~~~|$)/g, /`[^`\\n]*`/g];\n for (const re of patterns) {\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n ranges.push([m.index, m.index + m[0].length]);\n }\n }\n return ranges;\n}\n\nfunction isProtected(index: number, ranges: Array<[number, number]>): boolean {\n return ranges.some(([start, end]) => index >= start && index < end);\n}\n\nfunction preview(text: string, max = 160): string {\n const flat = text.replace(/\\s+/g, \" \").trim();\n return flat.length > max ? `${flat.slice(0, max)}…` : flat;\n}\n\n/**\n * Escapes `$$` delimiters that would make remark-math swallow non-math text.\n * Pure — safe to call on every render / stream chunk.\n */\nexport function guardMathDelimiters(text: string): DelimiterGuardResult {\n if (!text.includes(\"$$\")) return { text, violations: [] };\n\n const ranges = protectedRanges(text);\n\n // Collect `$$` offsets outside code.\n const tokens: number[] = [];\n for (let i = 0; i < text.length - 1; i++) {\n if (text[i] !== \"$\" || text[i + 1] !== \"$\") continue;\n if (!isProtected(i, ranges)) tokens.push(i);\n i++; // never treat the second `$` of a pair as a new opener\n }\n if (tokens.length === 0) return { text, violations: [] };\n\n const violations: DelimiterViolation[] = [];\n const escapeAt: number[] = [];\n\n let j = 0;\n while (j < tokens.length) {\n // Bounded by the loop condition — `tokens[j]` always exists here.\n const open = tokens[j] as number;\n const close = tokens[j + 1];\n\n if (close === undefined) {\n violations.push({\n reason: \"unpaired\",\n index: open,\n spanLength: 0,\n // An unpaired `$$` is inert to remark-math (nothing closes it), so it\n // is reported but NOT escaped — it already renders as literal text.\n preview: preview(text.slice(open, open + 120)),\n });\n break;\n }\n\n const inner = text.slice(open + 2, close);\n if (looksLikeMath(inner)) {\n j += 2;\n continue;\n }\n\n escapeAt.push(open);\n violations.push({\n reason: \"prose-span\",\n index: open,\n spanLength: inner.length,\n preview: preview(inner),\n });\n // Resume at the closing delimiter: it may legitimately open the NEXT span.\n j += 1;\n }\n\n let guarded = text;\n for (const index of [...escapeAt].sort((a, b) => b - a)) {\n guarded = `${guarded.slice(0, index)}${ESCAPED_DOLLARS}${guarded.slice(index + 2)}`;\n }\n\n return { text: guarded, violations };\n}\n\n/** Longest link label we accept before calling it a runaway. */\nconst MAX_LINK_LABEL = 200;\n\n/**\n * Block structure that can never legitimately sit inside a link label:\n * a blank line, a list item on its own line, or an ATX heading marker\n * (`## `…`#### `) anywhere — a heading inside a label always means the label\n * ran past its intended end.\n */\nconst LABEL_BLOCK_STRUCTURE =\n /\\n[ \\t]*\\n|(?:^|\\n)[ \\t]*[-*+][ \\t]+|#{2,6}[ \\t]/;\n\n/**\n * Escapes the `[` of a markdown link whose label ran away — the link twin of\n * the stray-`$$` bug. An unclosed citation bracket pairs with a `]` hundreds of\n * characters later and turns an entire section into one hyperlink.\n *\n * Pure. Runs after the math guard so both share one escaping pass conceptually,\n * but each is independently usable.\n */\nexport function guardRunawayLinks(text: string): DelimiterGuardResult {\n if (!text.includes(\"[\")) return { text, violations: [] };\n\n const ranges = protectedRanges(text);\n const violations: DelimiterViolation[] = [];\n const escapeAt: number[] = [];\n\n // `[label](target)` — label is non-greedy but may span newlines, which is\n // exactly the runaway shape we are looking for.\n const linkRe = /\\[((?:[^[\\]]|\\\\.)*)\\]\\(([^\\s)]*)/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(text)) !== null) {\n const open = m.index;\n if (isProtected(open, ranges)) continue;\n\n const label = m[1] ?? \"\";\n const runaway =\n label.length > MAX_LINK_LABEL || LABEL_BLOCK_STRUCTURE.test(label);\n if (!runaway) continue;\n\n escapeAt.push(open);\n violations.push({\n reason: \"runaway-link\",\n index: open,\n spanLength: label.length,\n preview: preview(label),\n });\n }\n\n let guarded = text;\n for (const index of [...escapeAt].sort((a, b) => b - a)) {\n guarded = `${guarded.slice(0, index)}${ESCAPED_BRACKET}${guarded.slice(index + 1)}`;\n }\n\n return { text: guarded, violations };\n}\n\n/**\n * The front door: run every delimiter guard in order. Offsets in the returned\n * violations refer to each guard's own input, so they are for diagnostics only.\n */\nexport function guardMarkdownDelimiters(text: string): DelimiterGuardResult {\n const math = guardMathDelimiters(text);\n const links = guardRunawayLinks(math.text);\n return {\n text: links.text,\n violations: [...math.violations, ...links.violations],\n };\n}\n\n/**\n * The payload handed to the injected capture sink — exactly the shape the\n * original passed to the Matrx `captureError` store. A Matrx host passes\n * `captureError` straight through; any host can log/report it its own way.\n */\nexport interface DelimiterCaptureInput {\n source: \"markdown-delimiters\";\n message: string;\n relation: string;\n details: string;\n conversationId?: string | undefined;\n callSite: \"guardMarkdownDelimiters\";\n raw: { messageId?: string | undefined; violations: DelimiterViolation[] };\n}\n\nexport interface DelimiterReportContext {\n renderPath: string;\n messageId?: string | undefined;\n conversationId?: string | undefined;\n /**\n * Optional error-capture sink (a Matrx host passes its `captureError`).\n * Absent, the loud recovery is console-only. Must never be relied on to\n * throw — failures inside it are swallowed so capture can never break\n * rendering.\n */\n capture?: ((input: DelimiterCaptureInput) => void) | undefined;\n}\n\n/**\n * Loud recovery. A firing means malformed math delimiters reached the renderer\n * — the guard kept the message readable, but the producer is still emitting\n * broken content and must be found.\n */\nexport function reportDelimiterViolations(\n violations: DelimiterViolation[],\n context: DelimiterReportContext,\n): void {\n if (violations.length === 0) return;\n try {\n const worst =\n violations.find((v) => v.reason !== \"unpaired\") ?? violations[0];\n if (!worst) return;\n const message =\n worst.reason === \"prose-span\"\n ? `Malformed math delimiters: a stray \"$$\" would have turned ${worst.spanLength} chars of prose into a math span (KaTeX would render it as red error text). Escaped it.`\n : worst.reason === \"runaway-link\"\n ? `Runaway markdown link: an unclosed \"[\" would have turned ${worst.spanLength} chars into one link label. Escaped it.`\n : `Malformed math delimiters: an unpaired \"$$\" reached the renderer.`;\n\n // Loud recovery: this is a defect being reported, not noise.\n console.warn(`[markdown-delimiter-guard] ${message}`, {\n renderPath: context.renderPath,\n violations,\n });\n\n context.capture?.({\n source: \"markdown-delimiters\",\n message,\n relation: `markdown:${context.renderPath}`,\n details: worst.preview,\n conversationId: context.conversationId,\n callSite: \"guardMarkdownDelimiters\",\n raw: { messageId: context.messageId, violations },\n });\n } catch {\n // Capture must never break rendering.\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @ai-matrx/kit/delimiter-guard — stops ONE stray markdown delimiter from
3
+ * swallowing a whole section of a streamed answer.
4
+ *
5
+ * THE FAILURE CLASS
6
+ * -----------------
7
+ * Markdown delimiters pair greedily and blindly. A single stray opener emitted
8
+ * by a model (the common shape is a mangled citation:
9
+ * `…/a-quicker-way-to-heal-prp-and-prf$$ .`) pairs with the next matching
10
+ * delimiter anywhere later in the message, and everything in between —
11
+ * headings, bold, links, whole sections — collapses into one node.
12
+ *
13
+ * Two delimiters cause this in a remark-math + CommonMark pipeline:
14
+ *
15
+ * 1. `$$` (remark-math). The swallowed prose becomes a math node, KaTeX fails
16
+ * to parse it, and `rehype-katex` falls back to its built-in error
17
+ * rendering: the raw source re-emitted inside `<span class="katex-error"
18
+ * style="color:#cc0000">`. The symptom is a huge block of BRIGHT RED
19
+ * unrendered markdown mid-answer. That red is KaTeX reporting a parse error
20
+ * on text that was never math — not a style of yours.
21
+ * 2. `[` (CommonMark link label). The swallowed prose becomes the label of one
22
+ * enormous hyperlink — the same bug wearing blue instead of red.
23
+ *
24
+ * WHAT THIS DOES
25
+ * --------------
26
+ * Before the markdown pipeline runs, each candidate span is checked for
27
+ * plausibility. A `$$…$$` span carrying markdown structure (links, URLs, bold,
28
+ * headings, list markers) or reading as prose is not math; a link label that is
29
+ * hundreds of characters long or contains block structure is not a label. The
30
+ * offending OPENER is neutralized (`&#36;&#36;`, `&#91;`) and scanning resumes at the next
31
+ * delimiter, so genuine math and genuine links later in the same message still
32
+ * render. Real content is never touched.
33
+ *
34
+ * LOUD RECOVERY: every firing is a real upstream defect (a model emitting
35
+ * malformed delimiters, or a producer mangling a citation). Callers report the
36
+ * returned violations — see `reportDelimiterViolations`.
37
+ *
38
+ * Ported verbatim from matrx-frontend `lib/markdown/delimiter-guard.ts`, with
39
+ * ONE coupling inversion: the app's `captureError` store import became the
40
+ * injected `capture` sink on `reportDelimiterViolations`' context (payload
41
+ * shape preserved exactly). No sink means console-only loud recovery.
42
+ */
43
+ type DelimiterViolationReason =
44
+ /** A `$$…$$` pair whose contents are prose/markdown, not math. */
45
+ "prose-span"
46
+ /** A `$$` with no closing partner in the content. */
47
+ | "unpaired"
48
+ /** A `[…](…)` link whose label swallowed prose/structure. */
49
+ | "runaway-link";
50
+ interface DelimiterViolation {
51
+ reason: DelimiterViolationReason;
52
+ /** Character offset of the offending delimiter in the input string. */
53
+ index: number;
54
+ /** Length of the span between the delimiters (0 for `unpaired`). */
55
+ spanLength: number;
56
+ /** Short excerpt of what would have been swallowed. */
57
+ preview: string;
58
+ }
59
+ interface DelimiterGuardResult {
60
+ /** Input with runaway openers escaped. */
61
+ text: string;
62
+ violations: DelimiterViolation[];
63
+ }
64
+ /**
65
+ * Escapes `$$` delimiters that would make remark-math swallow non-math text.
66
+ * Pure — safe to call on every render / stream chunk.
67
+ */
68
+ declare function guardMathDelimiters(text: string): DelimiterGuardResult;
69
+ /**
70
+ * Escapes the `[` of a markdown link whose label ran away — the link twin of
71
+ * the stray-`$$` bug. An unclosed citation bracket pairs with a `]` hundreds of
72
+ * characters later and turns an entire section into one hyperlink.
73
+ *
74
+ * Pure. Runs after the math guard so both share one escaping pass conceptually,
75
+ * but each is independently usable.
76
+ */
77
+ declare function guardRunawayLinks(text: string): DelimiterGuardResult;
78
+ /**
79
+ * The front door: run every delimiter guard in order. Offsets in the returned
80
+ * violations refer to each guard's own input, so they are for diagnostics only.
81
+ */
82
+ declare function guardMarkdownDelimiters(text: string): DelimiterGuardResult;
83
+ /**
84
+ * The payload handed to the injected capture sink — exactly the shape the
85
+ * original passed to the Matrx `captureError` store. A Matrx host passes
86
+ * `captureError` straight through; any host can log/report it its own way.
87
+ */
88
+ interface DelimiterCaptureInput {
89
+ source: "markdown-delimiters";
90
+ message: string;
91
+ relation: string;
92
+ details: string;
93
+ conversationId?: string | undefined;
94
+ callSite: "guardMarkdownDelimiters";
95
+ raw: {
96
+ messageId?: string | undefined;
97
+ violations: DelimiterViolation[];
98
+ };
99
+ }
100
+ interface DelimiterReportContext {
101
+ renderPath: string;
102
+ messageId?: string | undefined;
103
+ conversationId?: string | undefined;
104
+ /**
105
+ * Optional error-capture sink (a Matrx host passes its `captureError`).
106
+ * Absent, the loud recovery is console-only. Must never be relied on to
107
+ * throw — failures inside it are swallowed so capture can never break
108
+ * rendering.
109
+ */
110
+ capture?: ((input: DelimiterCaptureInput) => void) | undefined;
111
+ }
112
+ /**
113
+ * Loud recovery. A firing means malformed math delimiters reached the renderer
114
+ * — the guard kept the message readable, but the producer is still emitting
115
+ * broken content and must be found.
116
+ */
117
+ declare function reportDelimiterViolations(violations: DelimiterViolation[], context: DelimiterReportContext): void;
118
+
119
+ export { type DelimiterCaptureInput, type DelimiterGuardResult, type DelimiterReportContext, type DelimiterViolation, type DelimiterViolationReason, guardMarkdownDelimiters, guardMathDelimiters, guardRunawayLinks, reportDelimiterViolations };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @ai-matrx/kit/delimiter-guard — stops ONE stray markdown delimiter from
3
+ * swallowing a whole section of a streamed answer.
4
+ *
5
+ * THE FAILURE CLASS
6
+ * -----------------
7
+ * Markdown delimiters pair greedily and blindly. A single stray opener emitted
8
+ * by a model (the common shape is a mangled citation:
9
+ * `…/a-quicker-way-to-heal-prp-and-prf$$ .`) pairs with the next matching
10
+ * delimiter anywhere later in the message, and everything in between —
11
+ * headings, bold, links, whole sections — collapses into one node.
12
+ *
13
+ * Two delimiters cause this in a remark-math + CommonMark pipeline:
14
+ *
15
+ * 1. `$$` (remark-math). The swallowed prose becomes a math node, KaTeX fails
16
+ * to parse it, and `rehype-katex` falls back to its built-in error
17
+ * rendering: the raw source re-emitted inside `<span class="katex-error"
18
+ * style="color:#cc0000">`. The symptom is a huge block of BRIGHT RED
19
+ * unrendered markdown mid-answer. That red is KaTeX reporting a parse error
20
+ * on text that was never math — not a style of yours.
21
+ * 2. `[` (CommonMark link label). The swallowed prose becomes the label of one
22
+ * enormous hyperlink — the same bug wearing blue instead of red.
23
+ *
24
+ * WHAT THIS DOES
25
+ * --------------
26
+ * Before the markdown pipeline runs, each candidate span is checked for
27
+ * plausibility. A `$$…$$` span carrying markdown structure (links, URLs, bold,
28
+ * headings, list markers) or reading as prose is not math; a link label that is
29
+ * hundreds of characters long or contains block structure is not a label. The
30
+ * offending OPENER is neutralized (`&#36;&#36;`, `&#91;`) and scanning resumes at the next
31
+ * delimiter, so genuine math and genuine links later in the same message still
32
+ * render. Real content is never touched.
33
+ *
34
+ * LOUD RECOVERY: every firing is a real upstream defect (a model emitting
35
+ * malformed delimiters, or a producer mangling a citation). Callers report the
36
+ * returned violations — see `reportDelimiterViolations`.
37
+ *
38
+ * Ported verbatim from matrx-frontend `lib/markdown/delimiter-guard.ts`, with
39
+ * ONE coupling inversion: the app's `captureError` store import became the
40
+ * injected `capture` sink on `reportDelimiterViolations`' context (payload
41
+ * shape preserved exactly). No sink means console-only loud recovery.
42
+ */
43
+ type DelimiterViolationReason =
44
+ /** A `$$…$$` pair whose contents are prose/markdown, not math. */
45
+ "prose-span"
46
+ /** A `$$` with no closing partner in the content. */
47
+ | "unpaired"
48
+ /** A `[…](…)` link whose label swallowed prose/structure. */
49
+ | "runaway-link";
50
+ interface DelimiterViolation {
51
+ reason: DelimiterViolationReason;
52
+ /** Character offset of the offending delimiter in the input string. */
53
+ index: number;
54
+ /** Length of the span between the delimiters (0 for `unpaired`). */
55
+ spanLength: number;
56
+ /** Short excerpt of what would have been swallowed. */
57
+ preview: string;
58
+ }
59
+ interface DelimiterGuardResult {
60
+ /** Input with runaway openers escaped. */
61
+ text: string;
62
+ violations: DelimiterViolation[];
63
+ }
64
+ /**
65
+ * Escapes `$$` delimiters that would make remark-math swallow non-math text.
66
+ * Pure — safe to call on every render / stream chunk.
67
+ */
68
+ declare function guardMathDelimiters(text: string): DelimiterGuardResult;
69
+ /**
70
+ * Escapes the `[` of a markdown link whose label ran away — the link twin of
71
+ * the stray-`$$` bug. An unclosed citation bracket pairs with a `]` hundreds of
72
+ * characters later and turns an entire section into one hyperlink.
73
+ *
74
+ * Pure. Runs after the math guard so both share one escaping pass conceptually,
75
+ * but each is independently usable.
76
+ */
77
+ declare function guardRunawayLinks(text: string): DelimiterGuardResult;
78
+ /**
79
+ * The front door: run every delimiter guard in order. Offsets in the returned
80
+ * violations refer to each guard's own input, so they are for diagnostics only.
81
+ */
82
+ declare function guardMarkdownDelimiters(text: string): DelimiterGuardResult;
83
+ /**
84
+ * The payload handed to the injected capture sink — exactly the shape the
85
+ * original passed to the Matrx `captureError` store. A Matrx host passes
86
+ * `captureError` straight through; any host can log/report it its own way.
87
+ */
88
+ interface DelimiterCaptureInput {
89
+ source: "markdown-delimiters";
90
+ message: string;
91
+ relation: string;
92
+ details: string;
93
+ conversationId?: string | undefined;
94
+ callSite: "guardMarkdownDelimiters";
95
+ raw: {
96
+ messageId?: string | undefined;
97
+ violations: DelimiterViolation[];
98
+ };
99
+ }
100
+ interface DelimiterReportContext {
101
+ renderPath: string;
102
+ messageId?: string | undefined;
103
+ conversationId?: string | undefined;
104
+ /**
105
+ * Optional error-capture sink (a Matrx host passes its `captureError`).
106
+ * Absent, the loud recovery is console-only. Must never be relied on to
107
+ * throw — failures inside it are swallowed so capture can never break
108
+ * rendering.
109
+ */
110
+ capture?: ((input: DelimiterCaptureInput) => void) | undefined;
111
+ }
112
+ /**
113
+ * Loud recovery. A firing means malformed math delimiters reached the renderer
114
+ * — the guard kept the message readable, but the producer is still emitting
115
+ * broken content and must be found.
116
+ */
117
+ declare function reportDelimiterViolations(violations: DelimiterViolation[], context: DelimiterReportContext): void;
118
+
119
+ export { type DelimiterCaptureInput, type DelimiterGuardResult, type DelimiterReportContext, type DelimiterViolation, type DelimiterViolationReason, guardMarkdownDelimiters, guardMathDelimiters, guardRunawayLinks, reportDelimiterViolations };