diff --git a/README.md b/README.md index 35ff485cb..5d678a259 100644 --- a/README.md +++ b/README.md @@ -684,6 +684,8 @@ The Web UI composer also supports browser-based voice dictation when the browser Interrupted first-turn runs still persist a fallback conversation title, so stopping a streaming response does not leave the thread as "Untitled" after refresh. +Streaming Markdown responses animate only newly arrived words; text that is already visible is not faded out and replayed when the next chunk extends the same block. + In the Web UI, completed assistant turns can be branched into a new main conversation. The new thread starts from that turn's checkpoint. Because workspace files are not checkpointed, the branch only receives a best-effort copy of the current workspace when you branch from the latest turn; branching from an older turn keeps just the restored message history so the branch never inherits files that were created in a later part of the conversation. Web UI chat links percent-encode custom thread identifiers before placing them in route segments, so reserved URL characters such as `#` and `?` do not change which conversation is opened. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 07babdb86..e0864427d 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -86,6 +86,7 @@ Tool-calling AI messages can contain user-visible text as well as `tool_calls`. - **Thread hooks** (`useThreadStream`, `useSubmitThread`, `useThreads`) are the primary API interface - **Thread routes** — construct Web UI chat paths through `core/threads/utils.ts::pathOfThread()`, which percent-encodes both custom agent names and thread IDs before inserting them into route segments - **LangGraph client** is a singleton obtained via `getAPIClient()` in `core/api/` +- **Streaming Markdown rendering** is owned by `core/streamdown`: Streamdown's `animated` / `isAnimating` API handles incremental word animation, while the shared `streamdownRenderingPlugins` config registers the named code-highlighting and Mermaid plugins required by Streamdown 2.5. Keep wrappers and derived configs wired to that shared object; do not reintroduce a rehype plugin that wraps every word, because reparsing a growing block remounts old words and replays their animation. - **Environment validation** uses `@t3-oss/env-nextjs` with Zod schemas (`src/env.js`). Skip with `SKIP_ENV_VALIDATION=1` - **Subtask step history and runtime metadata** (`core/tasks/`) — the subtask card shows a subagent's full step timeline (#3779): its assistant reasoning turns interleaved with the tools it ran. `Subtask.steps[]` is accumulated live from `task_running` events (appended via `mergeSteps`, not overwritten) and backfilled on expand for historical runs by `fetchSubtaskSteps`, which pages the events endpoint scoped to one task (GET `/runs/{runId}/events?event_types=subagent.step&task_id=…&after_seq=…`) until a short page, so the run-wide limit can't truncate the timeline. `task_started` carries the effective `model_name`; `task_running` carries a cumulative usage snapshot after each completed LLM call. `core/tasks/lifecycle.ts` normalizes these additive events, and `computeNextSubtask` keeps the largest cumulative total so replayed or late SSE frames cannot double-count or roll the folded card backward. Terminal ToolMessage metadata (`subagent_model_name` / `subagent_token_usage`) restores the same values from normal history after reload; no per-card event fetch is needed. `core/tasks/steps.ts` is the pure step model: `messageToStep` (live), `eventsToSteps` (reload), `mergeSteps` (dedup by `message_index`), and `stepsForDisplay` (what the card renders — keeps tool steps + AI steps with text, drops the trailing final-answer AI step when completed since it's shown as `result`). `core/tasks/context.tsx`'s `useUpdateSubtask` applies updates against a `tasksRef` mirroring the latest state (not a closure snapshot), so a late-resolving `fetchSubtaskSteps` backfill merges into current state instead of clobbering SSE steps or sibling subtasks that arrived meanwhile. The owning `run_id` is carried onto history content messages in `buildVisibleHistoryMessages` so the card can resolve the events endpoint. diff --git a/frontend/package.json b/frontend/package.json index 7378d018a..f0297280c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,8 @@ "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", + "@streamdown/code": "1.1.1", + "@streamdown/mermaid": "1.0.2", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.90.17", "@types/hast": "^3.0.4", @@ -84,9 +86,9 @@ "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "shiki": "3.15.0", + "shiki": "3.23.0", "sonner": "^2.0.7", - "streamdown": "1.4.0", + "streamdown": "2.5.0", "tailwind-merge": "^3.4.0", "tokenlens": "^1.3.1", "unist-util-visit": "^5.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 001fe7156..a79934bda 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: '@radix-ui/react-use-controllable-state': specifier: ^1.2.2 version: 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@streamdown/code': + specifier: 1.1.1 + version: 1.1.1(react@19.2.4) + '@streamdown/mermaid': + specifier: 1.0.2 + version: 1.0.2(react@19.2.4) '@t3-oss/env-nextjs': specifier: ^0.12.0 version: 0.12.0(typescript@5.9.3)(zod@3.25.76) @@ -204,14 +210,14 @@ importers: specifier: ^6.0.0 version: 6.0.0 shiki: - specifier: 3.15.0 - version: 3.15.0 + specifier: 3.23.0 + version: 3.23.0 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) streamdown: - specifier: 1.4.0 - version: 1.4.0(@types/react@19.2.13)(react@19.2.4) + specifier: 2.5.0 + version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -738,105 +744,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -996,42 +986,36 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/simple-git-linux-arm64-musl@0.1.22': resolution: {integrity: sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/simple-git-linux-ppc64-gnu@0.1.22': resolution: {integrity: sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@napi-rs/simple-git-linux-s390x-gnu@0.1.22': resolution: {integrity: sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - libc: [glibc] '@napi-rs/simple-git-linux-x64-gnu@0.1.22': resolution: {integrity: sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/simple-git-linux-x64-musl@0.1.22': resolution: {integrity: sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/simple-git-win32-arm64-msvc@0.1.22': resolution: {integrity: sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==} @@ -1087,28 +1071,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -1736,28 +1716,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-arm64-musl@2.6.2': resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@resvg/resvg-js-linux-x64-gnu@2.6.2': resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-x64-musl@2.6.2': resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@resvg/resvg-js-win32-arm64-msvc@2.6.2': resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} @@ -1819,79 +1795,66 @@ packages: resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.2': resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.2': resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.2': resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.2': resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.2': resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.2': resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.2': resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.2': resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.2': resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.2': resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.2': resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.2': resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.62.2': resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} @@ -1955,25 +1918,21 @@ packages: resolution: {integrity: sha512-pSI+npPQE/uDtiboqvcOIRJbEV2+B+H1xffmko/gw50la92oTUW60kVULFwsb6L0+GVCzIcwX3yq60GtYIn+Ug==} cpu: [arm64] os: [linux] - libc: [glibc] '@rspack/binding-linux-arm64-musl@2.0.8': resolution: {integrity: sha512-igjJ43yxWQ72GZqjDDZSSHax9/Vg+6rLMmOvFglTJUkQpB4Tyvu/YjW+WRjYj2xRw6blOjLxUSJWASvuSqqlvg==} cpu: [arm64] os: [linux] - libc: [musl] '@rspack/binding-linux-x64-gnu@2.0.8': resolution: {integrity: sha512-zrkoEOnqj1hOEBO5T2I/2Ts2HSJsYFh1qXwMpK4dMJFGGNWDfNeUa6/LF5uq3VINF3JUl7RL47AgrucoSZJXPA==} cpu: [x64] os: [linux] - libc: [glibc] '@rspack/binding-linux-x64-musl@2.0.8': resolution: {integrity: sha512-6CtDaGZjNDvJd9TBp7a9zABbrPORO21W96+3ZcGBn0YNUPUk4ARxIxrTTpeJ/1F41QDM8AYIkGDdqEYMqTYBsA==} cpu: [x64] os: [linux] - libc: [musl] '@rspack/binding-wasm32-wasi@2.0.8': resolution: {integrity: sha512-Yf4SiqTUroT5Ju+te0YAY2xxKOb35tECsO21v7hYyGa705wrgoAK/MmF7enOvs9GR1iZIqgiLD/wxsIxl8GjJw==} @@ -2049,15 +2008,27 @@ packages: '@shikijs/engine-javascript@3.15.0': resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + '@shikijs/engine-oniguruma@3.15.0': resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + '@shikijs/langs@3.15.0': resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + '@shikijs/themes@3.15.0': resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + '@shikijs/twoslash@3.23.0': resolution: {integrity: sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g==} peerDependencies: @@ -2084,6 +2055,16 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@streamdown/code@1.1.1': + resolution: {integrity: sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/mermaid@1.0.2': + resolution: {integrity: sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2159,28 +2140,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -2584,49 +2561,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -3772,6 +3741,9 @@ packages: hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -4165,28 +4137,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -4231,11 +4199,6 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} - lucide-react@0.542.0: - resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.562.0: resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==} peerDependencies: @@ -4256,6 +4219,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -4942,12 +4910,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-markdown@10.1.0: - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - react-medium-image-zoom@5.4.1: resolution: {integrity: sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g==} peerDependencies: @@ -5036,8 +4998,8 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - rehype-harden@1.1.7: - resolution: {integrity: sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw==} + rehype-harden@1.1.8: + resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} @@ -5057,6 +5019,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} @@ -5088,6 +5053,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remend@1.3.0: + resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5212,6 +5180,9 @@ packages: shiki@3.15.0: resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -5279,10 +5250,11 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - streamdown@1.4.0: - resolution: {integrity: sha512-ylhDSQ4HpK5/nAH9v7OgIIdGJxlJB2HoYrYkJNGrO8lMpnWuKUcrz/A8xAMwA6eILA27469vIavcOTjmxctrKg==} + streamdown@2.5.0: + resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} peerDependencies: react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} @@ -6622,7 +6594,7 @@ snapshots: '@mdx-js/mdx@3.1.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 @@ -7602,19 +7574,38 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.4 + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + '@shikijs/engine-oniguruma@3.15.0': dependencies: '@shikijs/types': 3.15.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@3.15.0': dependencies: '@shikijs/types': 3.15.0 + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/themes@3.15.0': dependencies: '@shikijs/types': 3.15.0 + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/twoslash@3.23.0(typescript@5.9.3)': dependencies: '@shikijs/core': 3.23.0 @@ -7645,6 +7636,16 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@streamdown/code@1.1.1(react@19.2.4)': + dependencies: + react: 19.2.4 + shiki: 3.23.0 + + '@streamdown/mermaid@1.0.2(react@19.2.4)': + dependencies: + mermaid: 11.12.2 + react: 19.2.4 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -7920,7 +7921,7 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree@1.0.8': {} @@ -9301,7 +9302,7 @@ snapshots: estree-util-attach-comments@3.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-util-build-jsx@3.0.1: dependencies: @@ -9314,7 +9315,7 @@ snapshots: estree-util-scope@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 estree-util-to-js@2.0.0: @@ -9325,7 +9326,7 @@ snapshots: estree-util-value-to-estree@3.5.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-util-visit@2.0.0: dependencies: @@ -9336,7 +9337,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -9643,9 +9644,15 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-estree@3.1.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 @@ -9680,7 +9687,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/hast': 3.0.4 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 @@ -10102,10 +10109,6 @@ snapshots: lru-cache@11.5.2: {} - lucide-react@0.542.0(react@19.2.4): - dependencies: - react: 19.2.4 - lucide-react@0.562.0(react@19.2.4): dependencies: react: 19.2.4 @@ -10120,6 +10123,8 @@ snapshots: marked@16.4.2: {} + marked@17.0.6: {} + marky@1.3.0: {} math-intrinsics@1.1.0: {} @@ -10442,7 +10447,7 @@ snapshots: micromark-extension-mdx-expression@3.0.1: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.3 micromark-factory-space: 2.0.1 @@ -10453,7 +10458,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.3 @@ -10470,7 +10475,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-util-character: 2.1.1 @@ -10506,7 +10511,7 @@ snapshots: micromark-factory-mdx-expression@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 @@ -10570,7 +10575,7 @@ snapshots: micromark-util-events-to-acorn@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/unist': 3.0.3 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -11155,24 +11160,6 @@ snapshots: react-is@16.13.1: {} - react-markdown@10.1.0(@types/react@19.2.13)(react@19.2.4): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.2.13 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.1 - react: 19.2.4 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-medium-image-zoom@5.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -11220,7 +11207,7 @@ snapshots: recma-build-jsx@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-util-build-jsx: 3.0.1 vfile: 6.0.3 @@ -11235,14 +11222,14 @@ snapshots: recma-parse@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esast-util-from-js: 2.0.1 unified: 11.0.5 vfile: 6.0.3 recma-stringify@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-util-to-js: 2.0.0 unified: 11.0.5 vfile: 6.0.3 @@ -11277,7 +11264,7 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - rehype-harden@1.1.7: + rehype-harden@1.1.8: dependencies: unist-util-visit: 5.1.0 @@ -11315,12 +11302,17 @@ snapshots: rehype-recma@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/hast': 3.0.4 hast-util-to-estree: 3.1.3 transitivePeerDependencies: - supports-color + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + rehype-slug@6.0.0: dependencies: '@types/hast': 3.0.4 @@ -11402,6 +11394,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remend@1.3.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -11617,6 +11611,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -11688,24 +11693,27 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - streamdown@1.4.0(@types/react@19.2.13)(react@19.2.4): + streamdown@2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: clsx: 2.1.1 - katex: 0.16.28 - lucide-react: 0.542.0(react@19.2.4) - marked: 16.4.2 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + marked: 17.0.6 mermaid: 11.12.2 react: 19.2.4 - react-markdown: 10.1.0(@types/react@19.2.13)(react@19.2.4) - rehype-harden: 1.1.7 - rehype-katex: 7.0.1 + react-dom: 19.2.4(react@19.2.4) + rehype-harden: 1.1.8 rehype-raw: 7.0.0 + rehype-sanitize: 6.0.0 remark-gfm: 4.0.1 - remark-math: 6.0.0 - shiki: 3.15.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remend: 1.3.0 tailwind-merge: 3.4.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 transitivePeerDependencies: - - '@types/react' - supports-color string.prototype.codepointat@0.2.1: {} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 3a816c435..d35069fbe 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,4 +1,5 @@ import "katex/dist/katex.min.css"; +import "streamdown/styles.css"; import "@/styles/globals.css"; import { type Metadata } from "next"; diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index d77d75031..3d257e226 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -44,7 +44,10 @@ import { writeTextToClipboard } from "@/core/clipboard"; import { useI18n } from "@/core/i18n/hooks"; import { findToolCallResult } from "@/core/messages/utils"; import { installSkill, SkillRequestError } from "@/core/skills/api"; -import { SafeStreamdown } from "@/core/streamdown/components"; +import { + SafeStreamdown, + toStreamdownComponents, +} from "@/core/streamdown/components"; import { canBrowserPreviewFile, checkCodeFile, @@ -521,7 +524,7 @@ export function ArtifactFilePreview({ {content ?? ""} diff --git a/frontend/src/components/workspace/messages/markdown-content.tsx b/frontend/src/components/workspace/messages/markdown-content.tsx index 0fe2609e7..1f3d81480 100644 --- a/frontend/src/components/workspace/messages/markdown-content.tsx +++ b/frontend/src/components/workspace/messages/markdown-content.tsx @@ -17,8 +17,13 @@ import { type ClipboardSafeStreamdownProps } from "@/components/ai-elements/stre import { preprocessStreamdownMarkdown, streamdownPluginsWithoutRawHtml, + streamdownWordAnimation, } from "@/core/streamdown"; -import { SafeMessageResponse } from "@/core/streamdown/components"; +import { + SafeMessageResponse, + type StreamdownComponentOverrides, + toStreamdownComponents, +} from "@/core/streamdown/components"; import { cn } from "@/lib/utils"; import { createMarkdownLinkComponent } from "./markdown-link"; @@ -29,7 +34,7 @@ export type MarkdownContentProps = { rehypePlugins?: ClipboardSafeStreamdownProps["rehypePlugins"]; className?: string; remarkPlugins?: ClipboardSafeStreamdownProps["remarkPlugins"]; - components?: ClipboardSafeStreamdownProps["components"]; + components?: StreamdownComponentOverrides; }; type StreamingCodeProps = ComponentProps<"code"> & { @@ -260,8 +265,10 @@ export function MarkdownContent({ className={className} remarkPlugins={remarkPlugins} rehypePlugins={effectiveRehypePlugins} - components={components} - parseIncompleteMarkdown={isStreamingRender} + components={toStreamdownComponents(components)} + parseIncompleteMarkdown={isLoading} + animated={streamdownWordAnimation} + isAnimating={isLoading} > {normalizedContent} diff --git a/frontend/src/components/workspace/messages/message-group.tsx b/frontend/src/components/workspace/messages/message-group.tsx index 4340814c1..e75106b60 100644 --- a/frontend/src/components/workspace/messages/message-group.tsx +++ b/frontend/src/components/workspace/messages/message-group.tsx @@ -35,7 +35,6 @@ import { extractReasoningContentFromMessage, findToolCallResult, } from "@/core/messages/utils"; -import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { extractTitleFromMarkdown } from "@/core/utils/markdown"; import { env } from "@/env"; import { cn } from "@/lib/utils"; @@ -121,7 +120,6 @@ function MessageGroupComponent({ return filteredSteps[filteredSteps.length - 1]; } }, [lastToolCallStep, steps]); - const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const firstEligibleDebugSummaryStepIndexByMessageId = useMemo(() => { const firstIndices = new Map(); @@ -242,13 +240,7 @@ function MessageGroupComponent({ - } + label={} > ); @@ -269,7 +261,6 @@ function MessageGroupComponent({ } >, @@ -390,7 +381,6 @@ function MessageGroupComponent({ } > diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx index 6209bc722..0b28bfc2a 100644 --- a/frontend/src/components/workspace/messages/message-list-item.tsx +++ b/frontend/src/components/workspace/messages/message-list-item.tsx @@ -45,7 +45,6 @@ import { stripUploadedFilesTag, type FileInMessage, } from "@/core/messages/utils"; -import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { readReferenceMessageContexts } from "@/core/sidecar"; import { parseSlashSkillReference, @@ -295,7 +294,6 @@ function MessageContent_({ runId?: string; turnStartTime?: number | null; }) { - const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const isHuman = message.type === "human"; const rawTurnDuration = message.additional_kwargs?.turn_duration as | number @@ -478,7 +476,6 @@ function MessageContent_({ diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx index 08501c3ab..b1591a5ad 100644 --- a/frontend/src/components/workspace/messages/message-list.tsx +++ b/frontend/src/components/workspace/messages/message-list.tsx @@ -58,7 +58,6 @@ import { isHiddenFromUIMessage, type MessageGroup as ThreadMessageGroup, } from "@/core/messages/utils"; -import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { buildMessageSidecarContext, type SidecarContext, @@ -434,7 +433,6 @@ export function MessageList({ .slice(lastHumanIndex) .some((g) => g.type === "assistant"); }, [groupedMessages]); - const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const updateSubtask = useUpdateSubtask(); const lastGroupIndex = groupedMessages.length - 1; const turnUsageMessagesByGroupIndex = @@ -1036,7 +1034,6 @@ export function MessageList({ {renderTokenUsage({ messages: group.messages, @@ -1060,7 +1057,6 @@ export function MessageList({ )} diff --git a/frontend/src/components/workspace/messages/subtask-card.tsx b/frontend/src/components/workspace/messages/subtask-card.tsx index 3f5c37f0b..bced00578 100644 --- a/frontend/src/components/workspace/messages/subtask-card.tsx +++ b/frontend/src/components/workspace/messages/subtask-card.tsx @@ -20,9 +20,14 @@ import { ShineBorder } from "@/components/ui/shine-border"; import { useI18n } from "@/core/i18n/hooks"; import { hasToolCalls } from "@/core/messages/utils"; import { useModels } from "@/core/models/hooks"; -import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; -import { streamdownPluginsWithWordAnimation } from "@/core/streamdown"; -import { SafeStreamdown } from "@/core/streamdown/components"; +import { + streamdownPluginsWithoutRawHtml, + streamdownWordAnimation, +} from "@/core/streamdown"; +import { + SafeStreamdown, + toStreamdownComponents, +} from "@/core/streamdown/components"; import { fetchSubtaskSteps } from "@/core/tasks/api"; import { useSubtask, useUpdateSubtask } from "@/core/tasks/context"; import { @@ -53,7 +58,6 @@ export function SubtaskCard({ }) { const { t } = useI18n(); const [collapsed, setCollapsed] = useState(true); - const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const task = useSubtask(taskId)!; const { models, tokenUsageEnabled } = useModels(); const updateSubtask = useUpdateSubtask(); @@ -195,8 +199,10 @@ export function SubtaskCard({ {task.prompt} @@ -221,11 +227,7 @@ export function SubtaskCard({ (step.tool_name ?? t.subtasks[task.status]) ) : (
- +
) } @@ -242,11 +244,7 @@ export function SubtaskCard({ + ) : null } > diff --git a/frontend/src/core/rehype/index.ts b/frontend/src/core/rehype/index.ts deleted file mode 100644 index bac5d497e..000000000 --- a/frontend/src/core/rehype/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Element, Root, ElementContent } from "hast"; -import { useMemo } from "react"; -import { visit } from "unist-util-visit"; -import type { BuildVisitor } from "unist-util-visit"; - -const CJK_TEXT_RE = - /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u; - -export function rehypeSplitWordsIntoSpans() { - return (tree: Root) => { - visit(tree, "element", ((node: Element) => { - if ( - ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "strong"].includes( - node.tagName, - ) && - node.children - ) { - const newChildren: Array = []; - node.children.forEach((child) => { - if (child.type === "text") { - if (CJK_TEXT_RE.test(child.value)) { - newChildren.push(child); - return; - } - const segmenter = new Intl.Segmenter("zh", { granularity: "word" }); - const segments = segmenter.segment(child.value); - const words = Array.from(segments) - .map((segment) => segment.segment) - .filter(Boolean); - words.forEach((word: string) => { - newChildren.push({ - type: "element", - tagName: "span", - properties: { - className: "animate-fade-in", - }, - children: [{ type: "text", value: word }], - }); - }); - } else { - newChildren.push(child); - } - }); - node.children = newChildren; - } - }) as BuildVisitor); - }; -} - -export function useRehypeSplitWordsIntoSpans(enabled = true) { - const rehypePlugins = useMemo( - () => (enabled ? [rehypeSplitWordsIntoSpans] : []), - [enabled], - ); - return rehypePlugins; -} diff --git a/frontend/src/core/streamdown/components.tsx b/frontend/src/core/streamdown/components.tsx index 0dcc6c668..ce8d2ba21 100644 --- a/frontend/src/core/streamdown/components.tsx +++ b/frontend/src/core/streamdown/components.tsx @@ -1,9 +1,8 @@ "use client"; -import { - MessageResponse, - type MessageResponseProps, -} from "@/components/ai-elements/message"; +import type { ComponentType, JSX } from "react"; +import type { Components, ExtraProps } from "streamdown"; + import { ReasoningContent, type ReasoningContentProps, @@ -12,30 +11,73 @@ import { ClipboardSafeStreamdown, type ClipboardSafeStreamdownProps, } from "@/components/ai-elements/streamdown"; +import { cn } from "@/lib/utils"; +import { streamdownRenderingPlugins } from "./plugins"; import { useSafeStreamdownChildren, useSafeStreamdownMarkdown, } from "./safe-children"; +export type StreamdownComponentOverrides = { + [Key in keyof JSX.IntrinsicElements]?: + | ComponentType + | keyof JSX.IntrinsicElements; +} & { + inlineCode?: ComponentType; +}; + +/** + * Adapts normal intrinsic-element overrides to Streamdown's component map. + * + * Streamdown 2.5's public `Components` type combines those overrides with a + * catch-all index signature whose generic props reject correctly typed React + * components. The runtime contract still accepts this standard component map, + * so keep the compatibility cast at this boundary instead of every caller. + */ +export function toStreamdownComponents( + components: StreamdownComponentOverrides, +): Components { + return components as unknown as Components; +} + export function SafeStreamdown({ children, + plugins = streamdownRenderingPlugins, ...props }: ClipboardSafeStreamdownProps) { const safeChildren = useSafeStreamdownChildren(children); return ( - {safeChildren} + + {safeChildren} + ); } export function SafeMessageResponse({ children, + className, + plugins = streamdownRenderingPlugins, ...props -}: MessageResponseProps) { +}: ClipboardSafeStreamdownProps) { const safeChildren = useSafeStreamdownChildren(children); - return {safeChildren}; + // Keep this wrapper outside the registry-generated MessageResponse component. + // That component memoizes only by children, while Streamdown's animation + // lifecycle also needs isAnimating/animated prop changes to render. + return ( + *:first-child]:mt-0 [&>*:last-child]:mb-0", + className, + )} + plugins={plugins} + {...props} + > + {safeChildren} + + ); } export function SafeReasoningContent({ diff --git a/frontend/src/core/streamdown/plugins.ts b/frontend/src/core/streamdown/plugins.ts index 3eda6373d..b0784cb69 100644 --- a/frontend/src/core/streamdown/plugins.ts +++ b/frontend/src/core/streamdown/plugins.ts @@ -1,11 +1,11 @@ +import { code } from "@streamdown/code"; +import { mermaid } from "@streamdown/mermaid"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import type { StreamdownProps } from "streamdown"; -import { rehypeSplitWordsIntoSpans } from "../rehype"; - const katexOptions = { output: "html", throwOnError: false, @@ -17,7 +17,13 @@ const sharedRemarkPlugins = [ [remarkMath, { singleDollarTextMath: true }], ] as StreamdownProps["remarkPlugins"]; +export const streamdownRenderingPlugins = { + code, + mermaid, +} satisfies NonNullable; + export const streamdownPlugins = { + plugins: streamdownRenderingPlugins, remarkPlugins: sharedRemarkPlugins, rehypePlugins: [ rehypeRaw, @@ -25,15 +31,14 @@ export const streamdownPlugins = { ] as StreamdownProps["rehypePlugins"], }; -export const streamdownPluginsWithWordAnimation = { - remarkPlugins: sharedRemarkPlugins, - rehypePlugins: [ - [rehypeKatex, katexOptions], - rehypeSplitWordsIntoSpans, - ] as StreamdownProps["rehypePlugins"], -}; +export const streamdownWordAnimation = { + animation: "fadeIn", + duration: 200, + sep: "word", +} as const satisfies Exclude; export const streamdownPluginsWithoutRawHtml = { + plugins: streamdownPlugins.plugins, remarkPlugins: streamdownPlugins.remarkPlugins, rehypePlugins: streamdownPlugins.rehypePlugins?.filter( (p) => p !== rehypeRaw, diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index f3d79eaa7..cecc2ab7e 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -1,7 +1,9 @@ @import "tailwindcss"; @import "tw-animate-css"; -@source "../node_modules/streamdown/dist/index.js"; +@source "../../node_modules/streamdown/dist/index.js"; +@source "../../node_modules/@streamdown/code/dist/*.js"; +@source "../../node_modules/@streamdown/mermaid/dist/*.js"; /* Heading */ @source inline("text-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl}"); diff --git a/frontend/tests/e2e/thread-history-mermaid.spec.ts b/frontend/tests/e2e/thread-history-mermaid.spec.ts index 254b90f6c..e5eb60e6d 100644 --- a/frontend/tests/e2e/thread-history-mermaid.spec.ts +++ b/frontend/tests/e2e/thread-history-mermaid.spec.ts @@ -8,6 +8,10 @@ import { const mermaidContent = `Here is a relationship diagram. +\`\`\`typescript +const answer: number = 42; +\`\`\` + \`\`\`mermaid flowchart TD A[Lin
protagonist] @@ -87,4 +91,22 @@ test("historical run messages preview labelled dotted Mermaid arrows", async ({ timeout: 15_000, }); await expect(page.getByText("Mermaid Error:")).toHaveCount(0); + const highlightedTokens = page.locator( + '[data-streamdown="code-block-body"] code > span > span', + ); + await expect + .poll(() => highlightedTokens.count(), { timeout: 15_000 }) + .toBeGreaterThan(1); + const tokenStyles = await highlightedTokens.evaluateAll((tokens) => + tokens.map((token) => ({ + color: getComputedStyle(token).color, + style: token.getAttribute("style"), + })), + ); + expect(new Set(tokenStyles.map(({ style }) => style)).size).toBeGreaterThan( + 1, + ); + expect(new Set(tokenStyles.map(({ color }) => color)).size).toBeGreaterThan( + 1, + ); }); diff --git a/frontend/tests/unit/components/workspace/messages/markdown-content.test.ts b/frontend/tests/unit/components/workspace/messages/markdown-content.test.ts index 314024492..a0dd79429 100644 --- a/frontend/tests/unit/components/workspace/messages/markdown-content.test.ts +++ b/frontend/tests/unit/components/workspace/messages/markdown-content.test.ts @@ -43,9 +43,9 @@ describe("MarkdownContent streaming code blocks", () => { expect(html).not.toContain('data-streaming-inline-code="true"'); }); - it("restores Streamdown highlighting after streaming finishes", () => { + it("restores Streamdown code rendering after streaming finishes", () => { const html = renderMarkdown( - ["```html", '
Hello
', "```"].join("\n"), + ["```typescript", "const answer: number = 42;", "```"].join("\n"), false, ); @@ -91,6 +91,23 @@ describe("MarkdownContent streaming code blocks", () => { }); }); +describe("MarkdownContent streaming animation", () => { + it("uses Streamdown animation only for newly streamed words", () => { + const html = renderMarkdown("Hello streaming world", true); + + expect(html).toContain("data-sd-animate"); + expect(html).toContain("--sd-animation:sd-fadeIn"); + expect(html).toContain("--sd-duration:200ms"); + expect(html).not.toContain("animate-fade-in"); + }); + + it("does not animate completed markdown", () => { + const html = renderMarkdown("Hello completed world", false); + + expect(html).not.toContain("data-sd-animate"); + }); +}); + describe("MarkdownContent strikethrough", () => { it("preserves single tildes in temperature ranges", () => { const html = renderMarkdown("周六23~30℃;周日22~30℃", false); diff --git a/frontend/tests/unit/core/streamdown-plugins.test.ts b/frontend/tests/unit/core/streamdown-plugins.test.ts index a2f8a5e39..83227744b 100644 --- a/frontend/tests/unit/core/streamdown-plugins.test.ts +++ b/frontend/tests/unit/core/streamdown-plugins.test.ts @@ -4,13 +4,20 @@ import { renderToStaticMarkup } from "react-dom/server"; import { artifactMarkdownPlugins } from "@/components/workspace/artifacts/markdown-preview-plugins"; import { ArtifactLink } from "@/components/workspace/citations/artifact-link"; -import { SafeStreamdown, streamdownPlugins } from "@/core/streamdown"; +import { + SafeStreamdown, + streamdownPlugins, + toStreamdownComponents, +} from "@/core/streamdown"; function renderArtifactMarkdown(content: string) { return renderToStaticMarkup( createElement( SafeStreamdown, - { ...artifactMarkdownPlugins, components: { a: ArtifactLink } }, + { + ...artifactMarkdownPlugins, + components: toStreamdownComponents({ a: ArtifactLink }), + }, content, ), ); diff --git a/frontend/tests/unit/core/streamdown/plugins.test.ts b/frontend/tests/unit/core/streamdown/plugins.test.ts index cb27294ad..804866e8f 100644 --- a/frontend/tests/unit/core/streamdown/plugins.test.ts +++ b/frontend/tests/unit/core/streamdown/plugins.test.ts @@ -1,26 +1,65 @@ import { expect, test } from "@rstest/core"; +import { code, type HighlightResult } from "@streamdown/code"; +import { mermaid } from "@streamdown/mermaid"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; import { reasoningPlugins, streamdownPlugins, - streamdownPluginsWithWordAnimation, + streamdownRenderingPlugins, + streamdownWordAnimation, } from "@/core/streamdown/plugins"; test("shared streamdown configs disable single-tilde strikethrough", () => { const expectedGfmPlugin = [remarkGfm, { singleTilde: false }]; expect(streamdownPlugins.remarkPlugins).toContainEqual(expectedGfmPlugin); - expect(streamdownPluginsWithWordAnimation.remarkPlugins).toContainEqual( - expectedGfmPlugin, - ); +}); + +test("streaming word animation uses Streamdown's stable incremental animation", () => { + expect(streamdownWordAnimation).toEqual({ + animation: "fadeIn", + duration: 200, + sep: "word", + }); }); test("streamdownPlugins includes rehypeRaw", () => { expect(streamdownPlugins.rehypePlugins).toContain(rehypeRaw); }); +test("shared streamdown configs register code highlighting and Mermaid", () => { + expect(streamdownRenderingPlugins).toEqual({ code, mermaid }); + expect(streamdownPlugins.plugins).toBe(streamdownRenderingPlugins); + expect(reasoningPlugins.plugins).toBe(streamdownRenderingPlugins); +}); + +test("the shared code plugin produces highlighted Shiki tokens", async () => { + const source = "const answer: number = 42;"; + const highlighted = await new Promise((resolve) => { + const cached = code.highlight( + { + code: source, + language: "typescript", + themes: code.getThemes(), + }, + resolve, + ); + if (cached) { + resolve(cached); + } + }); + + expect( + highlighted.tokens + .flat() + .some((token) => + /^#[0-9A-Fa-f]{6}$/.test(String(token.htmlStyle?.color ?? "")), + ), + ).toBe(true); +}); + test("reasoningPlugins does not include rehypeRaw", () => { const flat = reasoningPlugins.rehypePlugins?.flat(); expect(flat).not.toContain(rehypeRaw);