diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index cb16a408d..c955eb427 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-tB0WZG6F6VFMS5zFqo73giSAn7SqfkJISF5fMjDioJQ= +sha256-m6anpp3ESvWCVQAoTsOhnTMpoewhpJTeVha2Gv4Xnc4= diff --git a/package-lock.json b/package-lock.json index 2147d093a..de6b745bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,19 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@anthropic-ai/sdk": { "version": "0.104.2", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.104.2.tgz", @@ -2362,6 +2375,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@clack/core": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", @@ -2523,14 +2548,6 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20260509.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260509.1.tgz", - "integrity": "sha512-jFlTTD+0MK/01TdL5sHIsQ8RqzfmvBsGl4hSp87INv2+JIs/JF6EL9J8enuCz6z3fNdfOKISNbGCIrzZRXVrcw==", - "license": "MIT OR Apache-2.0", - "optional": true, - "peer": true - }, "node_modules/@codemirror/autocomplete": { "version": "6.20.3", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", @@ -6422,6 +6439,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@ide/backoff": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", @@ -7728,6 +7762,15 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", @@ -12313,6 +12356,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -12380,6 +12676,12 @@ "@types/node": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -12684,6 +12986,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/turndown": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", @@ -13459,6 +13768,16 @@ "win32" ] }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@urql/core": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/@urql/core/-/core-4.0.11.tgz", @@ -16389,6 +16708,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -16612,6 +16940,505 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -16737,6 +17564,12 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -16979,6 +17812,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -17288,6 +18130,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -18118,6 +18969,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -18337,21 +19198,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-config-prettier": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -22002,6 +22848,12 @@ } } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -22561,7 +23413,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -22649,6 +23500,16 @@ "node": ">=4" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -22737,6 +23598,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -24344,6 +25214,31 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keychain": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/keychain/-/keychain-1.5.0.tgz", @@ -24361,6 +25256,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -24442,6 +25342,12 @@ "lan-network": "dist/lan-network-cli.js" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -24965,6 +25871,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -25247,6 +26159,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", @@ -25710,6 +26634,48 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -28213,6 +29179,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz", + "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -28378,6 +29350,12 @@ "npm": ">5" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -28744,6 +29722,22 @@ "node": ">=14.19.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -29673,18 +30667,6 @@ "react-native-worklets": "0.8.x" } }, - "node_modules/react-native-reanimated/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", @@ -29836,18 +30818,6 @@ "react-native": "0.81 - 0.85" } }, - "node_modules/react-native-worklets/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", @@ -30729,6 +31699,12 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -30791,6 +31767,18 @@ "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==", "license": "MIT" }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -30847,6 +31835,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -32318,6 +33312,12 @@ "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", "license": "MIT" }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -32765,7 +33765,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -32980,6 +33979,15 @@ "typescript": ">=4.0.0" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-deepmerge": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.0.tgz", @@ -35145,6 +36153,7 @@ "i18next": "^26.3.0", "lucide-react-native": "^0.546.0", "markdown-it": "^10.0.0", + "mermaid": "^11.16.0", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -35187,6 +36196,7 @@ "@xterm/headless": "^6.1.0-beta.213", "dotenv": "^17.2.3", "eas-cli": "^16.24.1", + "esbuild": "0.28.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "expo-gradle-jvmargs": "^1.1.2", @@ -35285,15 +36295,6 @@ "node": ">=16" } }, - "packages/app/node_modules/@cloudflare/workers-types": { - "version": "4.20260627.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260627.1.tgz", - "integrity": "sha512-QhVvier1eW9Ydw96N/Ez79i5CSGy7h39JucrUejQmAWQw9qBdlB1aOTjaD93EGYrV0t9U81YWetI3LcaxxILNw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "peer": true - }, "packages/app/node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -37569,209 +38570,6 @@ "vitest": "^4.1.6" } }, - "packages/highlight/node_modules/@vitest/expect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/@vitest/pretty-format": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/@vitest/runner": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.6", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/@vitest/snapshot": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.6", - "@vitest/utils": "4.1.6", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/@vitest/spy": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/@vitest/utils": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.6", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/highlight/node_modules/vitest": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.6", - "@vitest/mocker": "4.1.6", - "@vitest/pretty-format": "4.1.6", - "@vitest/runner": "4.1.6", - "@vitest/snapshot": "4.1.6", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.6", - "@vitest/browser-preview": "4.1.6", - "@vitest/browser-webdriverio": "4.1.6", - "@vitest/coverage-istanbul": "4.1.6", - "@vitest/coverage-v8": "4.1.6", - "@vitest/ui": "4.1.6", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "packages/highlight/node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.6", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "packages/protocol": { "name": "@getpaseo/protocol", "version": "0.3.1", @@ -39525,115 +40323,6 @@ "undici-types": "~6.21.0" } }, - "packages/website/node_modules/@vitest/browser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz", - "integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.10" - } - }, - "packages/website/node_modules/@vitest/browser-playwright": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", - "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/browser": "4.1.10", - "@vitest/mocker": "4.1.10", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.10" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } - } - }, - "packages/website/node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "packages/website/node_modules/@vitest/browser/node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "packages/website/node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", diff --git a/packages/app/e2e/browser/mermaid-streaming.spec.ts b/packages/app/e2e/browser/mermaid-streaming.spec.ts new file mode 100644 index 000000000..a9f87862c --- /dev/null +++ b/packages/app/e2e/browser/mermaid-streaming.spec.ts @@ -0,0 +1,68 @@ +import { test } from "../support/fixtures"; +import { + expectCompletedDiagram, + expectDiagramRemainsRenderedWhileStreaming, + expectDiagramWithLabels, + reloadConversation, + requestDiagram, + waitForDiagramTurnToComplete, +} from "../support/helpers/diagram"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +const STREAMED_MERMAID = [ + "```mermaid", + "flowchart LR", + " Start --> Middle", + ' Middle --> Done["Done"]', + " Middle --> Review", + " Review --> Verify", + " Verify --> Ship", + " Ship --> Package", + " Package --> Sign", + " Sign --> Upload", + " Upload --> Publish", + " Publish --> Deploy", + " Deploy --> Observe", + " Observe --> Validate", + " Validate --> Announce", + " Announce --> Document", + " Document --> Archive", + " Archive --> Release", + "```", +].join("\n"); + +test("keeps a Mermaid diagram rendered while its message streams, completes, and reloads", async ({ + page, +}) => { + const agent = await seedMockAgentWorkspace({ + repoPrefix: "mermaid-streaming-", + title: "Streaming Mermaid", + featureValues: { + mockStreamingAssistantResponse: STREAMED_MERMAID, + mockStreamingAssistantIntervalMs: 150, + }, + }); + try { + await test.step("Open the conversation and request a diagram", async () => { + await openAgentRoute(page, agent); + await requestDiagram(agent); + }); + + await test.step("The diagram renders and stays rendered while tokens arrive", async () => { + await expectDiagramWithLabels(page, ["Start", "Middle"]); + await expectDiagramRemainsRenderedWhileStreaming(page); + }); + + await test.step("The completed diagram shows the final streamed content", async () => { + await waitForDiagramTurnToComplete(agent); + await expectCompletedDiagram(page, ["Start", "Done", "Release"]); + }); + + await test.step("The completed diagram remains rendered after reload", async () => { + await reloadConversation(page); + await expectCompletedDiagram(page, ["Start", "Done", "Release"]); + }); + } finally { + await agent.cleanup(); + } +}); diff --git a/packages/app/e2e/support/helpers/diagram.ts b/packages/app/e2e/support/helpers/diagram.ts new file mode 100644 index 000000000..f3d2742c1 --- /dev/null +++ b/packages/app/e2e/support/helpers/diagram.ts @@ -0,0 +1,48 @@ +import { expect, type Page } from "@playwright/test"; +import type { MockAgentWorkspace } from "./mock-agent"; + +const DIAGRAM_NAME = "Diagram"; + +function renderedDiagram(page: Page) { + const diagram = page.getByRole("img", { name: DIAGRAM_NAME }).last(); + const svg = diagram.locator("iframe").contentFrame().locator("#diagram svg"); + return { diagram, svg }; +} + +export async function requestDiagram(agent: MockAgentWorkspace): Promise { + await agent.client.sendAgentMessage(agent.agentId, "Render the requested Mermaid diagram."); +} + +export async function expectDiagramWithLabels( + page: Page, + labels: readonly string[], +): Promise { + const { diagram, svg } = renderedDiagram(page); + await expect(diagram).toBeVisible({ timeout: 30_000 }); + await expect(svg).toBeVisible({ timeout: 30_000 }); + for (const label of labels) { + await expect(svg).toContainText(label); + } +} + +export async function expectDiagramRemainsRenderedWhileStreaming(page: Page): Promise { + const { diagram, svg } = renderedDiagram(page); + const samples = 80; + for (let sample = 0; sample < samples; sample += 1) { + await expect(diagram).toBeVisible({ timeout: 100 }); + await expect(svg).toBeVisible({ timeout: 100 }); + await page.waitForTimeout(25); + } +} + +export async function waitForDiagramTurnToComplete(agent: MockAgentWorkspace): Promise { + await agent.client.waitForFinish(agent.agentId, 30_000); +} + +export async function expectCompletedDiagram(page: Page, labels: readonly string[]): Promise { + await expectDiagramWithLabels(page, labels); +} + +export async function reloadConversation(page: Page): Promise { + await page.reload(); +} diff --git a/packages/app/package.json b/packages/app/package.json index 6b1901f7c..cd15f23a4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -29,7 +29,8 @@ "build:web": "npm --prefix ../.. run build:app-deps && expo export --platform web", "profile:workspace-tabs": "node ./scripts/profile-workspace-tabs.mjs", "deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main", - "build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs" + "build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs", + "build:mermaid-runtime": "node ./src/components/markdown/fence/mermaid/build-runtime.mjs" }, "dependencies": { "@codemirror/commands": "6.10.4", @@ -92,6 +93,7 @@ "i18next": "^26.3.0", "lucide-react-native": "^0.546.0", "markdown-it": "^10.0.0", + "mermaid": "^11.16.0", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -134,6 +136,7 @@ "@xterm/headless": "^6.1.0-beta.213", "dotenv": "^17.2.3", "eas-cli": "^16.24.1", + "esbuild": "0.28.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "expo-gradle-jvmargs": "^1.1.2", diff --git a/packages/app/src/agent-stream/layout.test.ts b/packages/app/src/agent-stream/layout.test.ts index 2fce5c437..1cc645f16 100644 --- a/packages/app/src/agent-stream/layout.test.ts +++ b/packages/app/src/agent-stream/layout.test.ts @@ -143,6 +143,30 @@ function findLayoutItem(layout: StreamLayout, id: string): StreamLayoutItem { } describe("layoutStream", () => { + it.each(["web", "android"] as const)( + "marks only the active live-head assistant block as streaming on %s", + (platform) => { + const completed = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 }); + const live = assistantMessage("turn:block:1", 3, { groupId: "turn", index: 1 }); + const active = layoutFor({ + platform, + isTurnActive: true, + tail: [userMessage("u1", 1), completed], + head: [live], + }); + const complete = layoutFor({ + platform, + isTurnActive: false, + tail: [userMessage("u1", 1), completed], + head: [live], + }); + + expect(findLayoutItem(active, completed.id).phase).toBe("complete"); + expect(findLayoutItem(active, live.id).phase).toBe("streaming"); + expect(findLayoutItem(complete, live.id).phase).toBe("complete"); + }, + ); + it.each(["web", "android"] as const)( "keeps split assistant block spacing identical to unsplit history on %s", (platform) => { diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts index 9249f8159..990b1f807 100644 --- a/packages/app/src/agent-stream/layout.ts +++ b/packages/app/src/agent-stream/layout.ts @@ -26,6 +26,7 @@ export interface StreamLayoutItem { isLastInUserGroup: boolean; isLastInToolSequence: boolean; frameOrder: StreamFrameChildOrder; + phase: "streaming" | "complete"; } export interface StreamLayout { @@ -53,6 +54,7 @@ interface LayoutSegmentInput { boundaryBelowItem: StreamItem | null; boundaryAboveItems: StreamItem[] | null; boundaryAboveIndex: number | null; + phase: "streaming" | "complete"; } interface AssistantFooterSource { @@ -273,6 +275,7 @@ function layoutSegment(input: LayoutSegmentInput): StreamLayoutItem[] { isLastInUserGroup: item.kind === "user_message" && belowItem?.kind !== "user_message", isLastInToolSequence: isToolSequenceItem(item) && !isToolSequenceItem(belowItem), frameOrder: input.frameOrder, + phase: input.phase, }; }); } @@ -324,6 +327,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout { boundaryBelowItem: liveHeadBoundaryItem, boundaryAboveItems: null, boundaryAboveIndex: null, + phase: "complete", }); byKey.set(historyCacheKey, history); } @@ -342,6 +346,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout { boundaryBelowItem: null, boundaryAboveItems: input.history, boundaryAboveIndex: historyBoundaryIndex, + phase: input.isTurnActive ? "streaming" : "complete", }); return { diff --git a/packages/app/src/agent-stream/strategy-web.test.tsx b/packages/app/src/agent-stream/strategy-web.test.tsx index 61777cb21..98be7dd91 100644 --- a/packages/app/src/agent-stream/strategy-web.test.tsx +++ b/packages/app/src/agent-stream/strategy-web.test.tsx @@ -206,6 +206,78 @@ describe("createWebStreamStrategy", () => { expect(renderLiveHeadRow).toHaveBeenCalledTimes(2); }); + it("keeps a row mounted when it moves from the live head into mounted history", () => { + const strategy = createWebStreamStrategy({ isMobileBreakpoint: false }); + const viewportRef = React.createRef(); + const item = userMessage(1); + const mounted = vi.fn(); + const unmounted = vi.fn(); + function StatefulRow() { + React.useEffect(() => { + mounted(); + return unmounted; + }, []); + return
{item.id}
; + } + const renderers: StreamSegmentRenderers = { + ...createRenderers(vi.fn()), + renderHistoryMountedRow: () => , + renderLiveHeadRow: () => , + }; + const renderInput: Omit = { + agentId: "agent", + renderers, + listEmptyComponent: null, + viewportRef, + routeBottomAnchorRequest: null, + isAuthoritativeHistoryReady: true, + onNearBottomChange: vi.fn(), + onNearHistoryStart: vi.fn().mockReturnValue(true), + isLoadingOlderHistory: false, + hasOlderHistory: false, + olderHistoryProgressKey: null, + scrollEnabled: true, + listStyle: null, + baseListContentContainerStyle: null, + forwardListContentContainerStyle: null, + }; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + act(() => { + root?.render( + strategy.render({ + ...renderInput, + segments: { historyVirtualized: [], historyMounted: [], liveHead: [item] }, + boundary: { + hasVirtualizedHistory: false, + hasMountedHistory: false, + hasLiveHead: true, + }, + }), + ); + }); + expect(mounted).toHaveBeenCalledTimes(1); + + act(() => { + root?.render( + strategy.render({ + ...renderInput, + segments: { historyVirtualized: [], historyMounted: [item], liveHead: [] }, + boundary: { + hasVirtualizedHistory: false, + hasMountedHistory: true, + hasLiveHead: false, + }, + }), + ); + }); + + expect(mounted).toHaveBeenCalledTimes(1); + expect(unmounted).not.toHaveBeenCalled(); + }); + it("reports the live-head row as the reading position", () => { const strategy = createWebStreamStrategy({ isMobileBreakpoint: false }); const viewportRef = React.createRef(); diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index 08781f80b..db244719b 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -1068,6 +1068,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool )); }, [liveHeadRowRevision, renderLiveHeadRow, segments.liveHead]); + const mountedRows = useMemo( + () => [...mountedHistoryRows, ...liveHeadRows], + [liveHeadRows, mountedHistoryRows], + ); const liveAuxiliary = useMemo(() => { return renderLiveAuxiliary(); }, [renderLiveAuxiliary]); @@ -1119,8 +1123,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool })} ) : null} - {mountedHistoryRows} - {liveHeadRows} + {mountedRows} {liveAuxiliary} {shouldRenderEmpty ? listEmptyComponent : null} diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index 35a8afef5..7203ca380 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -653,6 +653,7 @@ const AgentStreamViewComponent = forwardRef ); diff --git a/packages/app/src/components/markdown/fence/index.tsx b/packages/app/src/components/markdown/fence/index.tsx new file mode 100644 index 000000000..846aa9135 --- /dev/null +++ b/packages/app/src/components/markdown/fence/index.tsx @@ -0,0 +1,42 @@ +import type { ComponentType } from "react"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { getMarkdownFenceLanguage } from "./language"; +import { MermaidFence } from "./mermaid"; +import type { MarkdownFenceRendererProps } from "./types"; + +export interface MarkdownFenceBlockProps extends MarkdownFenceRendererProps { + info: string | null | undefined; +} + +const diagramFences: Partial>> = { + mermaid: MermaidFence, +}; + +export function MarkdownFenceBlock({ + code, + info, + phase, + inheritedStyles, + textStyle, +}: MarkdownFenceBlockProps) { + const language = getMarkdownFenceLanguage(info); + const DiagramFence = language ? diagramFences[language] : undefined; + if (DiagramFence) { + return ( + + ); + } + return ( + + ); +} diff --git a/packages/app/src/components/markdown/fence/language.test.ts b/packages/app/src/components/markdown/fence/language.test.ts new file mode 100644 index 000000000..172f72c31 --- /dev/null +++ b/packages/app/src/components/markdown/fence/language.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { getMarkdownFenceLanguage } from "./language"; + +describe("getMarkdownFenceLanguage", () => { + it("normalizes the first info-string token", () => { + expect(getMarkdownFenceLanguage(" Sirena theme=dark ")).toBe("sirena"); + expect(getMarkdownFenceLanguage("TypeScript title=example")).toBe("typescript"); + }); + + it("returns null when no language is declared", () => { + expect(getMarkdownFenceLanguage("")).toBeNull(); + expect(getMarkdownFenceLanguage(null)).toBeNull(); + expect(getMarkdownFenceLanguage(undefined)).toBeNull(); + }); +}); diff --git a/packages/app/src/components/markdown/fence/language.ts b/packages/app/src/components/markdown/fence/language.ts new file mode 100644 index 000000000..fd414a5ac --- /dev/null +++ b/packages/app/src/components/markdown/fence/language.ts @@ -0,0 +1,3 @@ +export function getMarkdownFenceLanguage(info: string | null | undefined): string | null { + return info?.trim().split(/\s+/)[0]?.toLowerCase() || null; +} diff --git a/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs b/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs new file mode 100644 index 000000000..e4f61b889 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs @@ -0,0 +1,76 @@ +import esbuild from "esbuild"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const appRoot = path.resolve(__dirname, "../../../../.."); +const repoRoot = path.resolve(appRoot, "../.."); +const entry = path.join(__dirname, "runtime/entry.ts"); +const output = path.join(__dirname, "runtime/html.gen.ts"); + +const result = await esbuild.build({ + entryPoints: [entry], + bundle: true, + write: false, + format: "iife", + platform: "browser", + target: ["ios15", "chrome100"], + minify: true, + // The app's deployment floor is iOS 15.1 but structuredClone (used by + // mermaid) only exists from iOS 15.4; esbuild targets transform syntax, + // not runtime APIs. Mermaid only clones plain config objects, so a + // JSON-based fallback is sufficient. + banner: { + js: 'if(typeof globalThis.structuredClone!=="function"){globalThis.structuredClone=function(v){return v===undefined?v:JSON.parse(JSON.stringify(v));};}', + }, + logLevel: "info", +}); + +const rawJs = result.outputFiles[0]?.text; +if (!rawJs) { + throw new Error("Mermaid runtime bundle produced no JavaScript"); +} +// JSON.stringify below escapes the TypeScript representation, not the runtime +// HTML — an embedded closing-script sequence would still terminate the inline +// + +`; + +const contents = `// Generated by packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs. +// Do not edit by hand. + +export const mermaidRuntimeHtml = ${JSON.stringify(html)}; +`; + +await fs.writeFile(output, contents); +console.log(`Wrote ${path.relative(repoRoot, output)} (${html.length} bytes)`); diff --git a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx new file mode 100644 index 000000000..044341f3a --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx @@ -0,0 +1,379 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Modal, + Pressable, + StyleSheet as RNStyleSheet, + ScrollView, + View, + type TextStyle, + type ViewStyle, +} from "react-native"; +import { WebView, type WebViewMessageEvent } from "react-native-webview"; +import { Code, Workflow, X } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import type { Theme } from "@/styles/theme"; +import type { MarkdownFenceRendererProps } from "../types"; +import type { MermaidRenderRequest } from "./render-model"; +import { mermaidRuntimeHtml } from "./runtime/html.gen"; +import { + parseMermaidRuntimeMessage, + serializeMermaidRuntimeRenderMessage, + type MermaidRuntimeRenderMessage, +} from "./runtime/messages"; +import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; +import { useMermaidRenderModel } from "./use-render-model"; +import { getDiagramBoxStyle } from "./presentation"; + +interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { + colorScheme?: "light" | "dark"; +} + +const WEBVIEW_SOURCE = { html: mermaidRuntimeHtml }; +const WEBVIEW_ORIGIN_WHITELIST = ["*"]; +const MAX_PREVIEW_HEIGHT = 480; + +interface MermaidWebViewProps { + request: MermaidRenderRequest | null; + interactive: boolean; + onRendered: (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => void; + onRenderFailed: (revision: number) => void; + style?: ViewStyle; +} + +function MermaidWebView({ + request, + interactive, + onRendered, + onRenderFailed, + style, +}: MermaidWebViewProps) { + const webViewRef = useRef(null); + const driverRef = useRef(null); + driverRef.current ??= new MermaidRuntimeRequestDriver(); + + const sendRequest = useCallback( + (current: MermaidRenderRequest | null) => { + if (!current || !webViewRef.current) { + return; + } + const message: MermaidRuntimeRenderMessage = { + type: "render", + revision: current.revision, + source: current.source, + colorScheme: current.colorScheme, + interactive, + }; + const payload = serializeMermaidRuntimeRenderMessage(message); + webViewRef.current.injectJavaScript( + `window.__PASEO_MERMAID_RUNTIME_RECEIVE__ && window.__PASEO_MERMAID_RUNTIME_RECEIVE__(${payload}); true;`, + ); + }, + [interactive], + ); + + useEffect(() => { + sendRequest(driverRef.current?.update(request) ?? null); + }, [request, sendRequest]); + + const handleMessage = useCallback( + (event: WebViewMessageEvent) => { + let value: unknown; + try { + value = JSON.parse(event.nativeEvent.data); + } catch { + return; + } + const message = parseMermaidRuntimeMessage(value); + if (!message) { + return; + } + if (message.type === "bridgeReady") { + sendRequest(driverRef.current?.ready() ?? null); + return; + } + if (message.type === "renderError") { + onRenderFailed(message.revision); + sendRequest(driverRef.current?.settled(message.revision, false) ?? null); + return; + } + onRendered(message); + sendRequest(driverRef.current?.settled(message.revision, true) ?? null); + }, + [onRenderFailed, onRendered, sendRequest], + ); + const handleShouldStartLoad = useCallback( + (load: { url: string }) => load.url === "about:blank" || load.url.startsWith("data:"), + [], + ); + + return ( + + ); +} + +const webViewStyles = RNStyleSheet.create({ + webView: { backgroundColor: "transparent" }, +}); + +interface MermaidDiagramViewerProps { + code: string; + colorScheme: "light" | "dark"; + onClose: () => void; + inheritedStyles: TextStyle; + textStyle: TextStyle; +} + +function MermaidDiagramViewer({ + code, + colorScheme, + onClose, + inheritedStyles, + textStyle, +}: MermaidDiagramViewerProps) { + const { t } = useTranslation(); + const [showSource, setShowSource] = useState(false); + const toggleSource = useCallback(() => setShowSource((current) => !current), []); + const { request, rendered, renderFailed } = useMermaidRenderModel({ + source: code, + phase: "complete", + colorScheme, + }); + const handleRendered = useCallback( + (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => { + rendered({ + revision: message.revision, + source: message.source, + colorScheme: message.colorScheme, + dimensions: { height: message.height, width: message.width }, + }); + }, + [rendered], + ); + + return ( + + + + + {showSource ? ( + + + + ) : null} + + + + {showSource ? ( + + ) : ( + + )} + + + + + + + + ); +} + +const ThemedCloseIcon = withUnistyles(X); +const ThemedSourceIcon = withUnistyles(Code); +const ThemedDiagramIcon = withUnistyles(Workflow); +const closeIconColor = (theme: Theme) => ({ color: theme.colors.foreground }); + +const viewerStyles = StyleSheet.create((theme, runtime) => ({ + backdrop: { flex: 1, backgroundColor: theme.colors.surface0 }, + webView: { + flex: 1, + marginTop: runtime.insets.top, + marginBottom: runtime.insets.bottom, + }, + runtime: { flex: 1 }, + sourceOverlay: { + position: "absolute", + inset: 0, + backgroundColor: theme.colors.surface0, + }, + source: { + paddingTop: theme.spacing[12], + paddingHorizontal: theme.spacing[4], + paddingBottom: theme.spacing[4], + }, + actions: { + position: "absolute", + top: runtime.insets.top + theme.spacing[3], + right: theme.spacing[4], + flexDirection: "row", + gap: theme.spacing[2], + }, + actionButton: { + padding: theme.spacing[2], + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface2, + }, +})); + +function MermaidFenceHostImpl({ + code, + phase, + inheritedStyles, + textStyle, + colorScheme = "dark", +}: MermaidFenceHostImplProps) { + const { t } = useTranslation(); + const { state, request, rendered, renderFailed } = useMermaidRenderModel({ + source: code, + phase, + colorScheme, + }); + const [hasRuntimeContent, setHasRuntimeContent] = useState(false); + const handleRendered = useCallback( + (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => { + setHasRuntimeContent(true); + rendered({ + revision: message.revision, + source: message.source, + colorScheme: message.colorScheme, + dimensions: { height: message.height, width: message.width }, + }); + }, + [rendered], + ); + const [viewerOpen, setViewerOpen] = useState(false); + const openViewer = useCallback(() => setViewerOpen(true), []); + const closeViewer = useCallback(() => setViewerOpen(false), []); + const visible = state.visible; + const canShowDiagram = visible !== null && hasRuntimeContent; + const previewInnerStyle = useMemo( + () => + canShowDiagram && visible + ? { height: Math.min(visible.height, MAX_PREVIEW_HEIGHT) } + : previewStyles.measuringInner, + [canShowDiagram, visible], + ); + const diagramBoxStyle = getDiagramBoxStyle(textStyle); + + return ( + <> + {!canShowDiagram ? ( + + ) : null} + + + + + + {viewerOpen && canShowDiagram && visible ? ( + + ) : null} + + ); +} + +const previewStyles = RNStyleSheet.create({ + measuring: { + position: "absolute", + left: 0, + right: 0, + opacity: 0, + pointerEvents: "none", + }, + measuringInner: { height: 240 }, + preview: { overflow: "hidden" }, +}); + +const mapColorScheme = (theme: Theme) => ({ colorScheme: theme.colorScheme }); +const ThemedMermaidFenceHost = withUnistyles(MermaidFenceHostImpl); + +export function MermaidFenceHost(props: MarkdownFenceRendererProps) { + return ; +} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.tsx b/packages/app/src/components/markdown/fence/mermaid/host.tsx new file mode 100644 index 000000000..50e959674 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.tsx @@ -0,0 +1,13 @@ +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import type { MarkdownFenceRendererProps } from "../types"; + +export function MermaidFenceHost({ code, inheritedStyles, textStyle }: MarkdownFenceRendererProps) { + return ( + + ); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx new file mode 100644 index 000000000..c51ab7542 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx @@ -0,0 +1,490 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ComponentType, +} from "react"; +import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Code, Scan, Workflow, ZoomIn, ZoomOut } from "lucide-react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import type { Theme } from "@/styles/theme"; +import type { MarkdownFenceRendererProps } from "../types"; +import type { MermaidRenderRequest } from "./render-model"; +import { mermaidRuntimeHtml } from "./runtime/html.gen"; +import { parseMermaidRuntimeMessage, type MermaidRuntimeRenderMessage } from "./runtime/messages"; +import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; +import { useMermaidRenderModel } from "./use-render-model"; +import { getDiagramBoxStyle } from "./presentation"; + +interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { + colorScheme?: "light" | "dark"; +} + +interface MermaidIframeRuntimeProps { + request: MermaidRenderRequest | null; + height: number; + onRendered: (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => void; + onRenderFailed: (revision: number) => void; +} + +function MermaidIframeRuntime({ + request, + height, + onRendered, + onRenderFailed, +}: MermaidIframeRuntimeProps) { + const iframeRef = useRef(null); + const driverRef = useRef(null); + driverRef.current ??= new MermaidRuntimeRequestDriver(); + const iframeStyle = useMemo( + () => ({ + display: "block", + width: "100%", + height, + border: 0, + pointerEvents: "none", + background: "transparent", + }), + [height], + ); + + const sendRequest = useCallback((current: MermaidRenderRequest | null) => { + const target = iframeRef.current?.contentWindow; + if (!current || !target) { + return; + } + const message: MermaidRuntimeRenderMessage = { + type: "render", + revision: current.revision, + source: current.source, + colorScheme: current.colorScheme, + interactive: false, + }; + target.postMessage(message, "*"); + }, []); + + useEffect(() => { + sendRequest(driverRef.current?.update(request) ?? null); + }, [request, sendRequest]); + + useEffect(() => { + function receiveMessage(event: MessageEvent): void { + if (event.source !== iframeRef.current?.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (!message) { + return; + } + if (message.type === "bridgeReady") { + sendRequest(driverRef.current?.ready() ?? null); + return; + } + if (message.type === "renderError") { + onRenderFailed(message.revision); + sendRequest(driverRef.current?.settled(message.revision, false) ?? null); + return; + } + onRendered(message); + sendRequest(driverRef.current?.settled(message.revision, true) ?? null); + } + window.addEventListener("message", receiveMessage); + return () => window.removeEventListener("message", receiveMessage); + }, [onRenderFailed, onRendered, sendRequest]); + + return ( + `},\"putIntoIFrame\"),n$e=v((t,e,r,n,i)=>{let a=t.append(\"div\");a.attr(\"id\",r),n&&a.attr(\"style\",n);let s=a.append(\"svg\").attr(\"id\",e).attr(\"width\",\"100%\").attr(\"xmlns\",gNt);return i&&s.attr(\"xmlns:xlink\",i),s.append(\"g\"),t},\"appendDivSvgG\");function VK(t,e){return t.append(\"iframe\").attr(\"id\",e).attr(\"style\",\"width: 100%; height: 100%;\").attr(\"sandbox\",\"\")}v(VK,\"sandboxedIframe\");var INt=v((t,e,r,n)=>{t.getElementById(e)?.remove(),t.getElementById(r)?.remove(),t.getElementById(n)?.remove()},\"removeExistingElements\"),MNt=v(async function(t,e,r){lM();let n=qK(e);e=n.code;let i=It();ne.debug(i),e.length>(i?.maxTextSize??hNt)&&(e=dNt);let a=`#${t}`,s=\"i\"+t,o=\"#\"+s,l=\"d\"+t,u=\"#\"+l,h=v(()=>{let _=tt(f?o:u).node();_&&\"remove\"in _&&_.remove()},\"removeTempElements\"),d=tt(document.body),f=i.securityLevel===fNt,p=i.securityLevel===pNt,g=i.fontFamily;if(r!==void 0){if(r&&(r.innerHTML=\"\"),f){let R=VK(tt(r),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(r);n$e(d,t,l,`font-family: ${g}`,mNt)}else{if(INt(document,t,l,s),f){let R=VK(tt(document.body),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(\"body\");n$e(d,t,l)}let m,y;try{m=await GK.fromText(e,{title:n.title})}catch(R){if(i.suppressErrorRendering)throw h(),R;m=await GK.fromText(\"error\"),y=R}let x=d.select(u).node(),b=m.type,T=x.firstChild,k=T.firstChild,C=m.renderer.getClasses?.(e,m),w=_Nt(i,b,C,a),S=document.createElement(\"style\");S.innerHTML=w,T.insertBefore(S,k);try{await m.renderer.draw(e,t,\"11.16.0\",m)}catch(R){throw i.suppressErrorRendering?h():$Mt.draw(e,t,\"11.16.0\"),R}let A=d.select(`${u} svg`),L=m.db.getAccTitle?.(),M=m.db.getAccDescription?.();K$e(b,A,L,M),d.select(`[id=\"${t}\"]`).selectAll(\"foreignobject > *\").attr(\"xmlns\",yNt);let P=d.select(u).node().innerHTML;if(ne.debug(\"config.arrowMarkerAbsolute\",i.arrowMarkerAbsolute),P=RNt(P,f,yl(i.arrowMarkerAbsolute)),f){let R=d.select(u+\" svg\").node();P=LNt(P,R)}else p||(P=to.sanitize(P,{ADD_TAGS:kNt,ADD_ATTR:SNt,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));if(sNt(),y)throw y;return h(),{diagramType:b,svg:P,bindFunctions:m.db.bindFunctions}},\"render\");function X$e(t={}){let e=Jn({},t);e?.fontFamily&&!e.themeVariables?.fontFamily&&(e.themeVariables||(e.themeVariables={}),e.themeVariables.fontFamily=e.fontFamily),NJ(e),e?.theme&&e.theme in qc?e.themeVariables=qc[e.theme].getThemeVariables(e.themeVariables):e&&(e.themeVariables=qc.default.getThemeVariables(e.themeVariables));let r=typeof e==\"object\"?DJ(e):AD();rC(r.logLevel),lM()}v(X$e,\"initialize\");var j$e=v((t,e={})=>{let{code:r}=WK(t);return GK.fromText(r,e)},\"getDiagramFromText\");function K$e(t,e,r,n){W$e(e,t),q$e(e,r,n,e.attr(\"id\"))}v(K$e,\"addA11yInfo\");var Im=Object.freeze({render:MNt,parse:Y$e,getDiagramFromText:j$e,initialize:X$e,getConfig:It,setConfig:_D,getSiteConfig:AD,updateSiteConfig:OJ,reset:v(()=>{CC()},\"reset\"),globalReset:v(()=>{CC(Gp)},\"globalReset\"),defaultConfig:Gp});rC(It().logLevel);CC(It());var DNt=v((t,e,r)=>{ne.warn(t),fA(t)?(r&&r(t.str,t.hash),e.push({...t,message:t.str,error:t})):(r&&r(t),t instanceof Error&&e.push({str:t.message,message:t.message,hash:t.name,error:t}))},\"handleError\"),Z$e=v(async function(t={querySelector:\".mermaid\"}){try{await NNt(t)}catch(e){if(fA(e)&&ne.error(e.str),wd.parseError&&wd.parseError(e),!t.suppressErrors)throw ne.error(\"Use the suppressErrors option to suppress these errors\"),e}},\"run\"),NNt=v(async function({postRenderCallback:t,querySelector:e,nodes:r}={querySelector:\".mermaid\"}){let n=Im.getConfig();ne.debug(`${t?\"\":\"No \"}Callback function found`);let i;if(r)i=r;else if(e)i=document.querySelectorAll(e);else throw new Error(\"Nodes and querySelector are both undefined\");ne.debug(`Found ${i.length} diagrams`),n?.startOnLoad!==void 0&&(ne.debug(\"Start On Load: \"+n?.startOnLoad),Im.updateSiteConfig({startOnLoad:n?.startOnLoad}));let a=new Zt.InitIDGenerator(n.deterministicIds,n.deterministicIDSeed),s,o=[];for(let l of Array.from(i)){if(ne.info(\"Rendering diagram: \"+l.id),l.getAttribute(\"data-processed\"))continue;l.setAttribute(\"data-processed\",\"true\");let u=`mermaid-${a.next()}`;s=l.innerHTML,s=CA(Zt.entityDecode(s)).trim().replace(//gi,\"
\");let h=Zt.detectInit(s);h&&ne.debug(\"Detected early reinit: \",h);try{let{svg:d,bindFunctions:f}=await tFe(u,s,l);l.innerHTML=d,t&&await t(u),f&&f(l)}catch(d){DNt(d,o,wd.parseError)}}if(o.length>0)throw o[0]},\"runThrowsErrors\"),Q$e=v(function(t){Im.initialize(t)},\"initialize\"),ONt=v(async function(t,e,r){ne.warn(\"mermaid.init is deprecated. Please use run instead.\"),t&&Q$e(t);let n={postRenderCallback:r,querySelector:\".mermaid\"};typeof e==\"string\"?n.querySelector=e:e&&(e instanceof HTMLElement?n.nodes=[e]:n.nodes=e),await Z$e(n)},\"init\"),PNt=v(async(t,{lazyLoad:e=!0}={})=>{lM(),c5(...t),e===!1&&await iNt()},\"registerExternalDiagrams\"),J$e=v(function(){if(wd.startOnLoad){let{startOnLoad:t}=Im.getConfig();t&&wd.run().catch(e=>ne.error(\"Mermaid failed to initialize\",e))}},\"contentLoaded\");typeof document<\"u\"&&window.addEventListener(\"load\",J$e,!1);var BNt=v(function(t){wd.parseError=t},\"setParseErrorHandler\"),oM=[],zK=!1,eFe=v(async()=>{if(!zK){for(zK=!0;oM.length>0;){let t=oM.shift();if(t)try{await t()}catch(e){ne.error(\"Error executing queue\",e)}}zK=!1}},\"executeQueue\"),$Nt=v(async(t,e)=>new Promise((r,n)=>{let i=v(()=>new Promise((a,s)=>{Im.parse(t,e).then(o=>{a(o),r(o)},o=>{ne.error(\"Error parsing\",o),wd.parseError?.(o),s(o),n(o)})}),\"performCall\");oM.push(i),eFe().catch(n)}),\"parse\"),tFe=v((t,e,r)=>new Promise((n,i)=>{let a=v(()=>new Promise((s,o)=>{Im.render(t,e,r).then(l=>{s(l),n(l)},l=>{ne.error(\"Error parsing\",l),wd.parseError?.(l),o(l),i(l)})}),\"performCall\");oM.push(a),eFe().catch(i)}),\"render\"),FNt=v(()=>Object.keys(Bd).map(t=>({id:t})),\"getRegisteredDiagramsMetadata\"),wd={startOnLoad:!0,mermaidAPI:Im,parse:$Nt,render:tFe,init:ONt,run:Z$e,registerExternalDiagrams:PNt,registerLayoutLoaders:_G,initialize:Q$e,parseError:void 0,contentLoaded:J$e,setParseErrorHandler:BNt,detectType:wC,registerIconPacks:qy,getRegisteredDiagramsMetadata:FNt},UK=wd;function zNt(t){return typeof t==\"object\"&&t!==null}function GNt(t){return t===\"light\"||t===\"dark\"}function rFe(t){return!zNt(t)||t.type!==\"render\"||typeof t.revision!=\"number\"||!Number.isInteger(t.revision)||typeof t.source!=\"string\"||!GNt(t.colorScheme)||typeof t.interactive!=\"boolean\"?null:{type:\"render\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,interactive:t.interactive}}function YK(t){window.ReactNativeWebView?.postMessage?.(JSON.stringify(t)),window.parent!==window&&window.parent.postMessage(t,\"*\")}function VNt(t){UK.initialize({startOnLoad:!1,securityLevel:\"strict\",suppressErrorRendering:!0,theme:t===\"dark\"?\"dark\":\"default\",secure:[\"secure\",\"securityLevel\",\"startOnLoad\",\"maxTextSize\",\"suppressErrorRendering\",\"maxEdges\",\"theme\",\"themeVariables\",\"themeCSS\"],flowchart:{htmlLabels:!1},class:{htmlLabels:!1}})}function WNt(t){document.querySelector('meta[name=\"viewport\"]')?.setAttribute(\"content\",t?\"width=device-width, initial-scale=1, maximum-scale=8\":\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\")}var XK=0,cM=null,uM=!1,HK=!1;async function qNt(t){try{VNt(t.colorScheme);let{svg:e}=await UK.render(`paseo-mermaid-${t.revision}`,t.source);if(t.revision!==XK)return;WNt(t.interactive);let r=document.getElementById(\"diagram\");if(!r)return;r.innerHTML=e;let n=r.querySelector(\"svg\")?.getBoundingClientRect();YK({type:\"rendered\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,height:Math.ceil(n?.height??r.scrollHeight),width:Math.ceil(n?.width??r.scrollWidth)})}catch{t.revision===XK&&YK({type:\"renderError\",revision:t.revision})}}async function UNt(){if(!uM){uM=!0;try{for(;cM;){let t=cM;cM=null,await qNt(t)}}finally{uM=!1}}}function nFe(t){let e=rFe(t);e&&(XK=e.revision,cM=e,!(uM||HK)&&(HK=!0,window.setTimeout(()=>{HK=!1,UNt()},0)))}window.__PASEO_MERMAID_RUNTIME_RECEIVE__=nFe;window.addEventListener(\"message\",t=>{t.source===window.parent&&nFe(t.data)});YK({type:\"bridgeReady\"});})();\n/*! Bundled license information:\n\ndompurify/dist/purify.es.mjs:\n (*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE *)\n\nmermaid/dist/chunks/mermaid.core/chunk-ZIRB5QZD.mjs:\n (*! Bundled license information:\n \n js-yaml/dist/js-yaml.mjs:\n (*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT *)\n *)\n\nlodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" --repo lodash/lodash#4.18.1 -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n\ncytoscape/dist/cytoscape.esm.mjs:\n (*!\n Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable\n Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)\n Licensed under The MIT License (http://opensource.org/licenses/MIT)\n *)\n (*!\n Event object based on jQuery events, MIT license\n \n https://jquery.org/license/\n https://tldrlegal.com/license/mit-license\n https://github.com/jquery/jquery/blob/master/src/event.js\n *)\n (*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n (*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n\n@mermaid-js/parser/dist/chunks/mermaid-parser.core/chunk-KEIR6QF5.mjs:\n (*! Bundled license information:\n \n lodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n *)\n\nmermaid/dist/mermaid.core.mjs:\n (*! Check if previously processed *)\n (*!\n * Wait for document loaded before starting the execution\n *)\n*/\n\n \n"; diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts new file mode 100644 index 000000000..99b23f9d6 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts @@ -0,0 +1,91 @@ +import type { DiagramColorScheme, DiagramDimensions } from "../render-model"; + +export interface MermaidRuntimeRenderMessage { + type: "render"; + revision: number; + source: string; + colorScheme: DiagramColorScheme; + interactive: boolean; +} + +export type MermaidRuntimeMessage = + | { type: "bridgeReady" } + | ({ + type: "rendered"; + revision: number; + source: string; + colorScheme: DiagramColorScheme; + } & DiagramDimensions) + | { type: "renderError"; revision: number }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isColorScheme(value: unknown): value is DiagramColorScheme { + return value === "light" || value === "dark"; +} + +export function parseMermaidRuntimeRenderMessage( + value: unknown, +): MermaidRuntimeRenderMessage | null { + if ( + !isRecord(value) || + value.type !== "render" || + typeof value.revision !== "number" || + !Number.isInteger(value.revision) || + typeof value.source !== "string" || + !isColorScheme(value.colorScheme) || + typeof value.interactive !== "boolean" + ) { + return null; + } + return { + type: "render", + revision: value.revision, + source: value.source, + colorScheme: value.colorScheme, + interactive: value.interactive, + }; +} + +export function parseMermaidRuntimeMessage(value: unknown): MermaidRuntimeMessage | null { + if (!isRecord(value) || typeof value.type !== "string") { + return null; + } + if (value.type === "bridgeReady") { + return { type: "bridgeReady" }; + } + if ( + value.type === "renderError" && + typeof value.revision === "number" && + Number.isInteger(value.revision) + ) { + return { type: "renderError", revision: value.revision }; + } + if ( + value.type === "rendered" && + typeof value.revision === "number" && + Number.isInteger(value.revision) && + typeof value.source === "string" && + isColorScheme(value.colorScheme) && + typeof value.height === "number" && + Number.isFinite(value.height) && + typeof value.width === "number" && + Number.isFinite(value.width) + ) { + return { + type: "rendered", + revision: value.revision, + source: value.source, + colorScheme: value.colorScheme, + height: value.height, + width: value.width, + }; + } + return null; +} + +export function serializeMermaidRuntimeRenderMessage(message: MermaidRuntimeRenderMessage): string { + return JSON.stringify(message).replace(/<\/script/gi, "<\\/script"); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts new file mode 100644 index 000000000..f2e93e243 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import type { MermaidRenderRequest } from "../render-model"; +import { MermaidRuntimeRequestDriver } from "./request-driver"; + +function request(revision: number, source: string): MermaidRenderRequest { + return { revision, source, colorScheme: "dark" }; +} + +describe("MermaidRuntimeRequestDriver", () => { + it("tries queued streaming prefixes in order until the runtime renders one", () => { + const driver = new MermaidRuntimeRequestDriver(); + const first = request(1, "flow"); + const valid = request(2, "flowchart TD\nA --> B\n"); + const transient = request(3, "flowchart TD\nA --> B\nB --"); + + expect(driver.update(first)).toBeNull(); + expect(driver.update(valid)).toBeNull(); + expect(driver.update(transient)).toBeNull(); + expect(driver.ready()).toBe(valid); + expect(driver.settled(valid.revision, true)).toBe(transient); + }); + + it("keeps completed lines but collapses incomplete token revisions before readiness", () => { + const driver = new MermaidRuntimeRequestDriver(); + const partial = request(1, "flowchart TD\nA --"); + const completed = request(2, "flowchart TD\nA --> B\n"); + const nextPartial = request(3, "flowchart TD\nA --> B\nB --"); + const latestPartial = request(4, "flowchart TD\nA --> B\nB -->"); + + driver.update(partial); + driver.update(completed); + driver.update(nextPartial); + driver.update(latestPartial); + + expect(driver.ready()).toBe(completed); + expect(driver.settled(completed.revision, true)).toBe(latestPartial); + }); + + it("collapses later updates to the newest request after the first render", () => { + const driver = new MermaidRuntimeRequestDriver(); + const first = request(1, "flowchart TD\nA --> B"); + const second = request(2, "flowchart TD\nA --> B\nB --> C"); + const latest = request(3, "flowchart TD\nA --> B\nB --> D"); + + driver.update(first); + expect(driver.ready()).toBe(first); + expect(driver.settled(first.revision, true)).toBeNull(); + expect(driver.update(second)).toBe(second); + expect(driver.update(latest)).toBeNull(); + expect(driver.settled(second.revision, false)).toBe(latest); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts new file mode 100644 index 000000000..8e3058701 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts @@ -0,0 +1,63 @@ +import type { MermaidRenderRequest } from "../render-model"; + +function isCompletedLineCandidate(source: string): boolean { + return source.endsWith("\n") && source.trim().includes("\n"); +} + +export class MermaidRuntimeRequestDriver { + private readyState = false; + private renderedOnce = false; + private inFlight: MermaidRenderRequest | null = null; + private queued: MermaidRenderRequest[] = []; + + update(request: MermaidRenderRequest | null): MermaidRenderRequest | null { + if (!request || this.hasRevision(request.revision)) { + return null; + } + if (this.renderedOnce) { + this.queued = [request]; + } else { + const lastIndex = this.queued.length - 1; + const last = this.queued[lastIndex]; + if (last && !isCompletedLineCandidate(last.source)) { + this.queued[lastIndex] = request; + } else { + this.queued.push(request); + } + } + return this.takeNext(); + } + + ready(): MermaidRenderRequest | null { + this.readyState = true; + return this.takeNext(); + } + + settled(revision: number, rendered: boolean): MermaidRenderRequest | null { + if (this.inFlight?.revision !== revision) { + return null; + } + this.inFlight = null; + if (rendered) { + this.renderedOnce = true; + const latest = this.queued.at(-1); + this.queued = latest ? [latest] : []; + } + return this.takeNext(); + } + + private hasRevision(revision: number): boolean { + return ( + this.inFlight?.revision === revision || + this.queued.some((request) => request.revision === revision) + ); + } + + private takeNext(): MermaidRenderRequest | null { + if (!this.readyState || this.inFlight || this.queued.length === 0) { + return null; + } + this.inFlight = this.queued.shift() ?? null; + return this.inFlight; + } +} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts new file mode 100644 index 000000000..a11ffd509 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { mermaidRuntimeHtml } from "./html.gen"; + +describe("Mermaid runtime document", () => { + it("ships a closed network policy", () => { + expect(mermaidRuntimeHtml).toContain("default-src 'none'"); + expect(mermaidRuntimeHtml).toContain("connect-src 'none'"); + expect(mermaidRuntimeHtml).toContain("img-src data: blob:"); + expect(mermaidRuntimeHtml).toContain("font-src 'none'"); + expect(mermaidRuntimeHtml).toContain("frame-src 'none'"); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts new file mode 100644 index 000000000..1dda832eb --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts @@ -0,0 +1,119 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { mermaidRuntimeHtml } from "./html.gen"; +import { parseMermaidRuntimeMessage, type MermaidRuntimeMessage } from "./messages"; + +const mountedFrames: HTMLIFrameElement[] = []; + +function waitForRuntimeMessage( + frame: HTMLIFrameElement, + predicate: (message: MermaidRuntimeMessage) => boolean, +): Promise { + return new Promise((resolve, reject) => { + const timeout = window.setTimeout(() => { + window.removeEventListener("message", receive); + reject(new Error("Timed out waiting for Mermaid runtime")); + }, 10_000); + function receive(event: MessageEvent): void { + if (event.source !== frame.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (!message || !predicate(message)) { + return; + } + window.clearTimeout(timeout); + window.removeEventListener("message", receive); + resolve(message); + } + window.addEventListener("message", receive); + }); +} + +async function mountRuntime(): Promise { + const frame = document.createElement("iframe"); + frame.sandbox.add("allow-scripts"); + const ready = waitForRuntimeMessage(frame, (message) => message.type === "bridgeReady"); + frame.srcdoc = mermaidRuntimeHtml; + document.body.append(frame); + mountedFrames.push(frame); + await ready; + return frame; +} + +function render( + frame: HTMLIFrameElement, + input: { revision: number; source: string }, +): Promise { + const response = waitForRuntimeMessage( + frame, + (message) => + message.type !== "bridgeReady" && + "revision" in message && + message.revision === input.revision, + ); + frame.contentWindow?.postMessage( + { + type: "render", + revision: input.revision, + source: input.source, + colorScheme: "dark", + interactive: false, + }, + "*", + ); + return response; +} + +afterEach(() => { + for (const frame of mountedFrames.splice(0)) { + frame.remove(); + } +}); + +describe("Mermaid sandbox runtime", () => { + it("renders successive valid streaming prefixes and reports an invalid prefix", async () => { + const frame = await mountRuntime(); + const firstSource = "flowchart TD\nA --> B"; + const secondSource = `${firstSource}\nB --> C`; + + const first = await render(frame, { revision: 1, source: firstSource }); + const invalid = await render(frame, { revision: 2, source: "not a mermaid diagram" }); + const second = await render(frame, { revision: 3, source: secondSource }); + + expect(first).toMatchObject({ type: "rendered", revision: 1, source: firstSource }); + expect(invalid).toEqual({ type: "renderError", revision: 2 }); + expect(second).toMatchObject({ type: "rendered", revision: 3, source: secondSource }); + }); + + it("coalesces queued input and never reports an obsolete result", async () => { + const frame = await mountRuntime(); + const obsoleteSource = `flowchart TD\n${Array.from({ length: 250 }, (_, index) => `A${index} --> A${index + 1}`).join("\n")}`; + const currentSource = "flowchart TD\nCurrent --> Result"; + const obsoleteResponses: MermaidRuntimeMessage[] = []; + function collect(event: MessageEvent): void { + if (event.source !== frame.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (message && message.type !== "bridgeReady" && message.revision === 10) { + obsoleteResponses.push(message); + } + } + window.addEventListener("message", collect); + frame.contentWindow?.postMessage( + { + type: "render", + revision: 10, + source: obsoleteSource, + colorScheme: "dark", + interactive: false, + }, + "*", + ); + const current = await render(frame, { revision: 11, source: currentSource }); + window.removeEventListener("message", collect); + + expect(current).toMatchObject({ type: "rendered", revision: 11, source: currentSource }); + expect(obsoleteResponses).toEqual([]); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts new file mode 100644 index 000000000..48b844e1e --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { containsUnsafeMermaidSource } from "./source-policy"; + +describe("containsUnsafeMermaidSource", () => { + it("rejects resource-bearing constructs", () => { + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ img: "https://x/y.png" }')).toBe(true); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ icon: 'pack:name' }")).toBe(true); + expect( + containsUnsafeMermaidSource('%%{init: {"themeCSS": "a { color: red }"}}%%\ngraph TD'), + ).toBe(true); + expect(containsUnsafeMermaidSource("graph TD\n A[url(http://x)]")).toBe(true); + expect(containsUnsafeMermaidSource("graph TD\n A[@import 'x']")).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A["styled"]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A["<img src=x>"]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); + }); + + it("rejects all shape-data constructs, including yaml key evasions", () => { + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "img": "https://x/y.png" }')).toBe( + true, + ); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ 'img': 'https://x/y.png' }")).toBe( + true, + ); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u0069mg": "https://x/y.png" }'), + ).toBe(true); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u{69}mg": "https://x/y.png" }'), + ).toBe(true); + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "\\x69mg": "https://x/y.png" }')).toBe( + true, + ); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\U00000069mg": "https://attacker/x" }'), + ).toBe(true); + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "icon": "pack:name" }')).toBe(true); + expect( + containsUnsafeMermaidSource( + 'flowchart TD\n A@{ ? img # comment\n : "https://attacker/x" }', + ), + ).toBe(true); + expect( + containsUnsafeMermaidSource( + 'flowchart TD\n A@{ dummy: &k img\n ? *k # comment\n : "https://attacker/x" }', + ), + ).toBe(true); + }); + + it("fails closed for malformed or out-of-range escapes without throwing", () => { + expect(() => + containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]'), + ).not.toThrow(); + expect(containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]')).toBe(true); + expect(() => + containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]'), + ).not.toThrow(); + expect(containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]')).toBe(true); + }); + + it("allows ordinary diagrams including formatting-only labels", () => { + expect(containsUnsafeMermaidSource("flowchart TD\n A[Start] --> B{Choice}")).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["formatted"]')).toBe(false); + expect(containsUnsafeMermaidSource("sequenceDiagram\n Alice->>Bob: a < b and x > y")).toBe( + false, + ); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ shape: rect }")).toBe(true); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.ts new file mode 100644 index 000000000..df6ad0312 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/source-policy.ts @@ -0,0 +1,52 @@ +// Mermaid can fetch external resources while *rendering* — image shapes +// (`A@{ img: "url" }`) construct an Image and await decode before any output +// sanitization runs, and CSS can pull url()/@import — so a prompt-injected +// diagram could exfiltrate data in a request URL. securityLevel "strict" does +// not prevent this (mermaid-js/mermaid#7645). Mermaid 11.16 parses the shape +// metadata object with yaml.JSON_SCHEMA semantics, including aliases and +// explicit keys; matching those keys safely would require a real parser. We +// therefore reject any `@{ ... }` shape-data construct up front and fall back +// to the source code block. Formatting-only `
` and `` tags are common +// in generated labels and carry no resource-bearing attributes; all other +// tags (and entity-encoded text that could smuggle one) are rejected. +const UNSAFE_MERMAID_SOURCE = + /@\s*\{|url\s*\(|@import\b|themeCSS|&#|<(?!\/?(?:br|i)\s*\/?>)[a-z!/]/i; + +// Mermaid labels can contain escaped text. Decode the escape forms we care +// about before running the denylist so disguised HTML still gets caught. +// Invalid escape sequences fail closed and fall back to the source block. +function normalizeMermaidSource(code: string): string | null { + try { + return code + .replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (_, hex: string) => + decodeCodePointEscape(Number.parseInt(hex, 16)), + ) + .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => + String.fromCharCode(Number.parseInt(hex, 16)), + ) + .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex: string) => + String.fromCharCode(Number.parseInt(hex, 16)), + ) + .replace(/["'`\\]/g, ""); + } catch { + return null; + } +} + +function decodeCodePointEscape(value: number): string { + if (!Number.isInteger(value) || value < 0 || value > 0x10ffff) { + throw new RangeError("Invalid Unicode code point"); + } + return String.fromCodePoint(value); +} + +export function containsUnsafeMermaidSource(code: string): boolean { + if (UNSAFE_MERMAID_SOURCE.test(code)) { + return true; + } + const normalized = normalizeMermaidSource(code); + if (normalized === null) { + return true; + } + return UNSAFE_MERMAID_SOURCE.test(normalized); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts new file mode 100644 index 000000000..cb35549b1 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useMemo, useReducer } from "react"; +import { + createMermaidRenderModel, + getMermaidRenderRequest, + reduceMermaidRenderModel, + type DiagramColorScheme, + type DiagramDimensions, + type RenderedDiagram, +} from "./render-model"; +import { containsUnsafeMermaidSource } from "./source-policy"; +import type { MarkdownPhase } from "../types"; + +const renderCache = new Map(); +const RENDER_CACHE_LIMIT = 50; + +function cacheKey(source: string, colorScheme: DiagramColorScheme): string { + return `${colorScheme}\u0000${source}`; +} + +function readCachedRender(source: string, colorScheme: DiagramColorScheme): RenderedDiagram | null { + return renderCache.get(cacheKey(source, colorScheme)) ?? null; +} + +function cacheRender(rendered: RenderedDiagram): void { + if (renderCache.size >= RENDER_CACHE_LIMIT) { + const oldest = renderCache.keys().next().value; + if (oldest !== undefined) { + renderCache.delete(oldest); + } + } + renderCache.set(cacheKey(rendered.source, rendered.colorScheme), rendered); +} + +export function useMermaidRenderModel({ + source, + phase, + colorScheme, +}: { + source: string; + phase: MarkdownPhase; + colorScheme: DiagramColorScheme; +}) { + const renderInput = useMemo( + () => ({ + source, + phase, + colorScheme, + rejected: containsUnsafeMermaidSource(source), + cached: readCachedRender(source, colorScheme), + }), + [colorScheme, phase, source], + ); + const [state, dispatch] = useReducer( + reduceMermaidRenderModel, + renderInput, + createMermaidRenderModel, + ); + + useEffect(() => { + dispatch({ type: "inputChanged", input: renderInput }); + }, [renderInput]); + + const rendered = useCallback( + (response: { + revision: number; + source: string; + colorScheme: DiagramColorScheme; + dimensions: DiagramDimensions; + }) => { + const cached: RenderedDiagram = { + source: response.source, + colorScheme: response.colorScheme, + ...response.dimensions, + }; + cacheRender(cached); + dispatch({ + type: "rendered", + revision: response.revision, + source: response.source, + colorScheme: response.colorScheme, + dimensions: response.dimensions, + }); + }, + [], + ); + const renderFailed = useCallback((revision: number) => { + dispatch({ type: "renderFailed", revision }); + }, []); + + return { + state, + request: getMermaidRenderRequest(state), + rendered, + renderFailed, + }; +} diff --git a/packages/app/src/components/markdown/fence/types.ts b/packages/app/src/components/markdown/fence/types.ts new file mode 100644 index 000000000..b499bc893 --- /dev/null +++ b/packages/app/src/components/markdown/fence/types.ts @@ -0,0 +1,10 @@ +import type { TextStyle } from "react-native"; + +export type MarkdownPhase = "streaming" | "complete"; + +export interface MarkdownFenceRendererProps { + code: string; + phase: MarkdownPhase; + inheritedStyles: TextStyle; + textStyle: TextStyle; +} diff --git a/packages/app/src/components/markdown/renderer.tsx b/packages/app/src/components/markdown/renderer.tsx index 292ea6f76..ddb82ee33 100644 --- a/packages/app/src/components/markdown/renderer.tsx +++ b/packages/app/src/components/markdown/renderer.tsx @@ -23,6 +23,7 @@ import Markdown, { } from "react-native-markdown-display"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { MarkdownFenceBlock } from "@/components/markdown/fence"; import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; import { MarkdownTableCellText } from "@/components/markdown-text-selection"; import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; @@ -606,10 +607,11 @@ export function createSharedMarkdownRules(): RenderRules { styles: MarkdownStyles, inheritedStyles: TextStyle = {}, ) => ( - diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index e2e54fa77..1b19994c7 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -71,6 +71,8 @@ import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown- import { markdownNodeContainsType } from "@/utils/markdown-ast"; import { useStableEvent } from "@/hooks/use-stable-event"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { MarkdownFenceBlock } from "@/components/markdown/fence"; +import type { MarkdownPhase } from "@/components/markdown/fence/types"; import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; import { formatDuration, formatMessageTimestamp } from "@/utils/time"; @@ -739,6 +741,7 @@ interface AssistantMessageProps { serverId?: string; client?: DaemonClient | null; spacing?: "default" | "compactTop" | "compactBottom" | "compactBoth"; + phase: MarkdownPhase; } export const assistantMessageStylesheet = StyleSheet.create((theme) => ({ @@ -1450,6 +1453,7 @@ export const AssistantMessage = memo(function AssistantMessage({ serverId, client, spacing = "default", + phase, }: AssistantMessageProps) { const markdownParser = useMemo(createAssistantMarkdownParser, []); @@ -1695,10 +1699,11 @@ export const AssistantMessage = memo(function AssistantMessage({ styles: MarkdownStyles, inheritedStyles: TextStyle = {}, ) => ( - @@ -1893,11 +1898,11 @@ export const AssistantMessage = memo(function AssistantMessage({ ); }, }; - }, [client, fileLinkActions, markdownParser, occurrenceKey, serverId, workspaceRoot]); + }, [client, fileLinkActions, markdownParser, occurrenceKey, phase, serverId, workspaceRoot]); const blocks = useMemo(() => splitMarkdownBlocks(message), [message]); const keyedBlocks = useMemo( - () => blocks.map((block, index) => ({ key: `${index}:${block.slice(0, 32)}`, block })), + () => blocks.map((block, index) => ({ key: `block:${index}`, block })), [blocks], ); diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index c38381b72..810c8d3a1 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -272,6 +272,14 @@ export const ar: TranslationResources = { }, }, message: { + diagram: { + diagram: "مخطط", + zoomIn: "تكبير", + zoomOut: "تصغير", + resetZoom: "إعادة تعيين العرض", + viewSource: "عرض المصدر", + viewDiagram: "عرض المخطط", + }, actions: { copyCode: "نسخ الرمز", copyTurn: "نسخ بدوره", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index c6954b975..9caf7292b 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -271,6 +271,14 @@ export const en = { }, }, message: { + diagram: { + diagram: "Diagram", + zoomIn: "Zoom in", + zoomOut: "Zoom out", + resetZoom: "Reset view", + viewSource: "View source", + viewDiagram: "View diagram", + }, actions: { copyCode: "Copy code", copyTurn: "Copy turn", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 7b0fa3875..c5d4a2d27 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -275,6 +275,14 @@ export const es: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagrama", + zoomIn: "Acercar", + zoomOut: "Alejar", + resetZoom: "Restablecer vista", + viewSource: "Ver fuente", + viewDiagram: "Ver diagrama", + }, actions: { copyCode: "Copiar código", copyTurn: "Copiar turno", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 70d7e2cc1..bc22d3afe 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -276,6 +276,14 @@ export const fr: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagramme", + zoomIn: "Zoomer", + zoomOut: "Dézoomer", + resetZoom: "Réinitialiser la vue", + viewSource: "Afficher la source", + viewDiagram: "Afficher le diagramme", + }, actions: { copyCode: "Copier le code", copyTurn: "Copier le tour", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 85b95d85e..07308bb8f 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -275,6 +275,14 @@ export const ja: TranslationResources = { }, }, message: { + diagram: { + diagram: "図", + zoomIn: "拡大", + zoomOut: "縮小", + resetZoom: "表示をリセット", + viewSource: "ソースを表示", + viewDiagram: "図を表示", + }, actions: { copyCode: "コードをコピー", copyTurn: "ターンをコピー", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index cd8969955..2762f0174 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -273,6 +273,14 @@ export const ko: TranslationResources = { }, }, message: { + diagram: { + diagram: "다이어그램", + zoomIn: "확대", + zoomOut: "축소", + resetZoom: "보기 초기화", + viewSource: "소스 보기", + viewDiagram: "다이어그램 보기", + }, actions: { copyCode: "코드 복사", copyTurn: "턴 복사", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index aa9fb7a9a..4aeba0209 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -275,6 +275,14 @@ export const ptBR: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagrama", + zoomIn: "Aproximar", + zoomOut: "Afastar", + resetZoom: "Redefinir visualização", + viewSource: "Ver fonte", + viewDiagram: "Ver diagrama", + }, actions: { copyCode: "Copiar código", copyTurn: "Copiar turno", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 753ddafc2..37ab6b1cf 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -274,6 +274,14 @@ export const ru: TranslationResources = { }, }, message: { + diagram: { + diagram: "Диаграмма", + zoomIn: "Приблизить", + zoomOut: "Отдалить", + resetZoom: "Сбросить вид", + viewSource: "Показать код", + viewDiagram: "Показать диаграмму", + }, actions: { copyCode: "Скопировать код", copyTurn: "Копировать ход", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 29167ff8e..1d2bed925 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -272,6 +272,14 @@ export const zhCN: TranslationResources = { }, }, message: { + diagram: { + diagram: "图表", + zoomIn: "放大", + zoomOut: "缩小", + resetZoom: "重置视图", + viewSource: "查看源码", + viewDiagram: "查看图表", + }, actions: { copyCode: "复制代码", copyTurn: "复制回合", diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts index 99ce0596c..3e2374282 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts @@ -68,6 +68,47 @@ describe("MockLoadTestAgentClient", () => { await session.interrupt(); }); + test("streams a configured assistant response through the normal timeline", async () => { + vi.useFakeTimers(); + const response = [ + "```mermaid", + "flowchart LR", + " Start --> Middle", + ' Middle --> End["Done"]', + "```", + ].join("\n"); + const client = new MockLoadTestAgentClient(); + const session = await client.createSession({ + provider: "mock", + cwd: process.cwd(), + model: "ten-second-stream", + featureValues: { + mockStreamingAssistantResponse: response, + mockStreamingAssistantIntervalMs: 20, + }, + }); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + const resultPromise = session.run("Render a diagram while streaming."); + await vi.runAllTimersAsync(); + + await expect(resultPromise).resolves.toMatchObject({ finalText: response, canceled: false }); + const streamedText = events + .flatMap((event) => + event.type === "timeline" && event.item.type === "assistant_message" + ? [event.item.text] + : [], + ) + .join(""); + expect(streamedText).toBe(response); + expect( + events.filter((event) => event.type === "timeline" && event.item.type === "assistant_message") + .length, + ).toBeGreaterThan(5); + expect(events.at(-1)).toMatchObject({ type: "turn_completed", provider: "mock" }); + }); + test("can withhold the provider user-message echo until an immediate interrupt", async () => { vi.useFakeTimers(); const client = new MockLoadTestAgentClient(); diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts index 147045ea0..78a13ea18 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts @@ -644,6 +644,8 @@ export class MockLoadTestAgentSession implements AgentSession { private modeId: string | null; private modelId: string | null; private readonly assistantResponse: string | null; + private readonly streamingAssistantResponse: string | null; + private readonly streamingAssistantIntervalMs: number; private readonly rewindError: string | null; private remainingPromptRejections: number; @@ -656,6 +658,18 @@ export class MockLoadTestAgentSession implements AgentSession { typeof options.config.featureValues?.mockAssistantResponse === "string" ? options.config.featureValues.mockAssistantResponse : null; + this.streamingAssistantResponse = + typeof options.config.featureValues?.mockStreamingAssistantResponse === "string" + ? options.config.featureValues.mockStreamingAssistantResponse + : null; + const requestedStreamingInterval = + options.config.featureValues?.mockStreamingAssistantIntervalMs; + this.streamingAssistantIntervalMs = + typeof requestedStreamingInterval === "number" && + Number.isFinite(requestedStreamingInterval) && + requestedStreamingInterval >= 1 + ? Math.min(requestedStreamingInterval, 1_000) + : MOCK_LOAD_TEST_INTERVAL_MS; this.rewindError = typeof options.config.featureValues?.mockRewindError === "string" ? options.config.featureValues.mockRewindError @@ -721,6 +735,8 @@ export class MockLoadTestAgentSession implements AgentSession { const scheduleTurn = () => { if (shouldEmitTurnFailure(prompt)) { this.scheduleFailedTurn(turn); + } else if (this.streamingAssistantResponse !== null) { + this.scheduleStreamingAssistantTurn(turn, this.streamingAssistantResponse); } else if (this.assistantResponse !== null) { this.scheduleSettledAssistantTurn(turn, this.assistantResponse); } else if (structuredBranchName) { @@ -1040,6 +1056,32 @@ export class MockLoadTestAgentSession implements AgentSession { turn.timer.unref?.(); } + private scheduleStreamingAssistantTurn(turn: ActiveTurn, finalText: string): void { + const tokens = tokenize(finalText); + const emitNext = () => { + if (this.activeTurn !== turn) { + return; + } + this.clearTurnTimer(turn); + this.emitTurnStarted(turn); + const token = tokens.shift(); + if (token === undefined) { + this.finishTurnWithText(turn, finalText); + return; + } + turn.emittedTokens += 1; + this.emitTimeline(turn.turnId, { + type: "assistant_message", + text: token, + messageId: turn.assistantMessageId, + }); + turn.timer = setTimeout(emitNext, this.streamingAssistantIntervalMs); + turn.timer.unref?.(); + }; + turn.timer = setTimeout(emitNext, 0); + turn.timer.unref?.(); + } + private emitSettledAssistantTurn(turn: ActiveTurn, finalText: string): void { if (this.activeTurn !== turn) { return;