From 6376c6937899463b80e8b057d006329efcbdc602 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Wed, 24 Jun 2026 11:40:36 +0800 Subject: [PATCH 1/8] chore: upgrade @assistant-ui/react 0.12 to 0.14 and react-streamdown 0.1 to 0.3 bumps @assistant-ui/react from ^0.12.28 to ^0.14.23 and @assistant-ui/react-streamdown from ^0.1.11 to ^0.3.4. this crosses two minor bumps on each package and unlocks the built-in defer, smooth, and tail-bounded remend primitives for PR 2. breaking change from core 0.2.x: MessageRepository.appendOptimisticMessage was removed (assistant-ui#4162). inline the three steps it did (generateId + fromThreadMessageLike + addOrUpdateMessage) in incremental-external-store-runtime.ts, and set metadata.isOptimistic so the new off-branch eviction logic cleans up the placeholder correctly. fromThreadMessageLike and generateId graduated to the public API in 0.14.22 (assistant-ui#4414), so they now import from @assistant-ui/react instead of @assistant-ui/core/internal. ExportedMessageRepository in the test file moves to the public import for the same reason. the remaining internal imports (AssistantRuntimeImpl, BaseAssistantRuntimeCore, ExternalStoreThreadListRuntimeCore, ExternalStoreThreadRuntimeCore, hasUpcomingMessage) are runtime construction internals with no public equivalent and stay on @assistant-ui/core/internal. the @assistant-ui/store npm override is removed: all transitive ranges now resolve to 0.2.18 without it. verified: tsc --noEmit passes, vitest shows zero new failures (15 pre-existing, 792 passing, identical to baseline before the upgrade). --- apps/desktop/package.json | 4 +- .../thread/user-message-edit.test.tsx | 2 +- .../lib/incremental-external-store-runtime.ts | 16 +- package-lock.json | 1090 +++++------------ package.json | 1 - 5 files changed, 289 insertions(+), 824 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4bf4eaade965..df1ba07124c3 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -47,8 +47,8 @@ "preview": "node scripts/assert-root-install.cjs && vite preview --host 127.0.0.1 --port 4174" }, "dependencies": { - "@assistant-ui/react": "^0.12.28", - "@assistant-ui/react-streamdown": "^0.1.11", + "@assistant-ui/react": "^0.14.23", + "@assistant-ui/react-streamdown": "^0.3.4", "@audiowave/react": "^0.6.2", "@chenglou/pretext": "^0.0.6", "@codemirror/commands": "^6.10.4", diff --git a/apps/desktop/src/components/assistant-ui/thread/user-message-edit.test.tsx b/apps/desktop/src/components/assistant-ui/thread/user-message-edit.test.tsx index 914fc043b123..0528764fe541 100644 --- a/apps/desktop/src/components/assistant-ui/thread/user-message-edit.test.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/user-message-edit.test.tsx @@ -1,4 +1,4 @@ -import { ExportedMessageRepository } from '@assistant-ui/core/internal' +import { ExportedMessageRepository } from '@assistant-ui/react' // Clicking a user bubble must open the inline edit composer — through the // app's incremental external-store runtime (which reimplements capability // resolution, incl. `edit: onEdit !== undefined`) and the stock runtime. diff --git a/apps/desktop/src/lib/incremental-external-store-runtime.ts b/apps/desktop/src/lib/incremental-external-store-runtime.ts index c055175091dd..e8d2033c4dbd 100644 --- a/apps/desktop/src/lib/incremental-external-store-runtime.ts +++ b/apps/desktop/src/lib/incremental-external-store-runtime.ts @@ -8,6 +8,8 @@ import { import { type AssistantRuntime, type ExternalStoreAdapter, + fromThreadMessageLike, + generateId, type ThreadMessage, useRuntimeAdapters } from '@assistant-ui/react' @@ -135,10 +137,16 @@ class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRunti } if (hasUpcomingMessage(isRunning, messages)) { - self._assistantOptimisticId = this.repository.appendOptimisticMessage(messages.at(-1)?.id ?? null, { - role: 'assistant', - content: [] - }) + const optimisticId = generateId() + this.repository.addOrUpdateMessage( + messages.at(-1)?.id ?? null, + fromThreadMessageLike( + { role: 'assistant', content: [], metadata: { isOptimistic: true } }, + optimisticId, + { type: 'running' } + ) + ) + self._assistantOptimisticId = optimisticId } this.repository.resetHead(self._assistantOptimisticId ?? messages.at(-1)?.id ?? null) diff --git a/package-lock.json b/package-lock.json index a8c1ff7a5be4..66a167a98a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,8 +61,8 @@ "name": "hermes", "version": "0.17.0", "dependencies": { - "@assistant-ui/react": "^0.12.28", - "@assistant-ui/react-streamdown": "^0.1.11", + "@assistant-ui/react": "^0.14.23", + "@assistant-ui/react-streamdown": "^0.3.4", "@audiowave/react": "^0.6.2", "@chenglou/pretext": "^0.0.6", "@codemirror/commands": "^6.10.4", @@ -161,6 +161,150 @@ "node": "^20.19.0 || >=22.12.0" } }, + "apps/desktop/node_modules/@assistant-ui/core": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@assistant-ui/core/-/core-0.2.19.tgz", + "integrity": "sha512-QYwIy+l21rvVsyuc19doblCJ3bfhBzqbuNa/xdts8CIDlw+5LWg1uVGCiVhTRpkysHuJyONDF4TVBAxdMXR7yw==", + "license": "MIT", + "dependencies": { + "assistant-stream": "^0.3.24", + "nanoid": "^5.1.15" + }, + "peerDependencies": { + "@assistant-ui/store": "^0.2.13", + "@assistant-ui/tap": "^0.9.0", + "@types/react": "*", + "assistant-cloud": "^0.1.31", + "react": "^18 || ^19", + "zustand": "^5.0.11" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "assistant-cloud": { + "optional": true + }, + "react": { + "optional": true + }, + "zustand": { + "optional": true + } + } + }, + "apps/desktop/node_modules/@assistant-ui/react": { + "version": "0.14.24", + "resolved": "https://registry.npmjs.org/@assistant-ui/react/-/react-0.14.24.tgz", + "integrity": "sha512-DHUEbJfn3EeApiLXJp6pZfwzGUwQ7aAoGeUfs8bmo1G9uAkRlxF1RKr7MwM/oVSUt+ixSsfWey+OuHJi5gtKCQ==", + "license": "MIT", + "dependencies": { + "@assistant-ui/core": "^0.2.19", + "@assistant-ui/store": "^0.2.19", + "@assistant-ui/tap": "^0.9.3", + "@radix-ui/primitive": "^1.1.4", + "@radix-ui/react-compose-refs": "^1.1.3", + "@radix-ui/react-context": "^1.1.4", + "@radix-ui/react-primitive": "^2.1.6", + "@radix-ui/react-use-callback-ref": "^1.1.2", + "@radix-ui/react-use-escape-keydown": "^1.1.2", + "assistant-cloud": "^0.1.34", + "assistant-stream": "^0.3.24", + "nanoid": "^5.1.15", + "radix-ui": "^1.6.0", + "react-textarea-autosize": "^8.5.9", + "safe-content-frame": "^0.0.21", + "zod": "^4.4.3", + "zustand": "^5.0.14" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/desktop/node_modules/@assistant-ui/react-streamdown": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@assistant-ui/react-streamdown/-/react-streamdown-0.3.5.tgz", + "integrity": "sha512-xl4UoFGtCfeUk6T/F9d94djE4Vc2KPQ8I3vy7cV0fb8IGN63yHt/rVzehxMSX4XT81Qrpsa0KUQhJOo/8zxdkw==", + "license": "MIT", + "dependencies": { + "rehype-harden": "^1.1.8", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remend": "^1.3.0", + "streamdown": "^2.5.0" + }, + "peerDependencies": { + "@assistant-ui/react": "^0.14.18", + "@streamdown/cjk": "^1.0.0", + "@streamdown/code": "^1.0.0", + "@streamdown/math": "^1.0.0", + "@streamdown/mermaid": "^1.0.0", + "@types/react": "*", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@streamdown/cjk": { + "optional": true + }, + "@streamdown/code": { + "optional": true + }, + "@streamdown/math": { + "optional": true + }, + "@streamdown/mermaid": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "apps/desktop/node_modules/@assistant-ui/store": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@assistant-ui/store/-/store-0.2.19.tgz", + "integrity": "sha512-7GMEoK+H4iINquteLFXqqDB5SAzB19YHBy4KKQprxHseAtDrpkC8w9pTrPfJ1yLvbn3FqjXk645bCt5vDf1sTg==", + "license": "MIT", + "dependencies": { + "use-effect-event": "^2.0.3" + }, + "peerDependencies": { + "@assistant-ui/tap": "^0.9.0", + "@types/react": "*", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/desktop/node_modules/@assistant-ui/tap": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@assistant-ui/tap/-/tap-0.9.3.tgz", + "integrity": "sha512-IKIgDbaKUPvVt2hMKL+WU2E8oru5J3muO9iSjL/vEVf6TNz5H07wrOKwD+1xhitqR1Nc4WtHK986jqoeGmWRDw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "apps/desktop/node_modules/@nous-research/ui": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/@nous-research/ui/-/ui-0.13.2.tgz", @@ -307,151 +451,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@assistant-ui/core": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@assistant-ui/core/-/core-0.1.17.tgz", - "integrity": "sha512-IWIP98UVQ9W+oF0yz8XqFRtaX8HtozWVUWt6D/BSV6cyKwLfJ8niHtLG74bSnllTnGcreU2El3GR/tIodR1XuA==", - "license": "MIT", - "dependencies": { - "assistant-stream": "^0.3.12", - "nanoid": "^5.1.9" - }, - "peerDependencies": { - "@assistant-ui/store": "^0.2.9", - "@assistant-ui/tap": "^0.5.10", - "@types/react": "*", - "assistant-cloud": "^0.1.27", - "react": "^18 || ^19", - "zustand": "^5.0.11" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "assistant-cloud": { - "optional": true - }, - "react": { - "optional": true - }, - "zustand": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/react": { - "version": "0.12.28", - "resolved": "https://registry.npmjs.org/@assistant-ui/react/-/react-0.12.28.tgz", - "integrity": "sha512-czjpexLK1lKnNDNM1YMJi8SufeKUWBICqiVUtiHMV+86PYGCwJykOZKkchI8MVbSQ62xZ8A1LfPO5W2IDjed3A==", - "license": "MIT", - "dependencies": { - "@assistant-ui/core": "^0.1.17", - "@assistant-ui/store": "^0.2.9", - "@assistant-ui/tap": "^0.5.10", - "@radix-ui/primitive": "^1.1.3", - "@radix-ui/react-compose-refs": "^1.1.2", - "@radix-ui/react-context": "^1.1.3", - "@radix-ui/react-primitive": "^2.1.4", - "@radix-ui/react-use-callback-ref": "^1.1.1", - "@radix-ui/react-use-escape-keydown": "^1.1.1", - "assistant-cloud": "^0.1.27", - "assistant-stream": "^0.3.12", - "nanoid": "^5.1.9", - "radix-ui": "^1.4.3", - "react-textarea-autosize": "^8.5.9", - "zod": "^4.3.6", - "zustand": "^5.0.12" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/react-streamdown": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@assistant-ui/react-streamdown/-/react-streamdown-0.1.11.tgz", - "integrity": "sha512-9y+89ZxotYSt81hChSVjK2kwUYRKq7UW/r5qoqZTpcb7119gc0NOj0dx9xxuyXE2QfR6EY8rW6yBz3g+Y7RrhQ==", - "license": "MIT", - "dependencies": { - "rehype-harden": "^1.1.8", - "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", - "streamdown": "^2.5.0" - }, - "peerDependencies": { - "@assistant-ui/react": "^0.12.26", - "@streamdown/cjk": "^1.0.0", - "@streamdown/code": "^1.0.0", - "@streamdown/math": "^1.0.0", - "@streamdown/mermaid": "^1.0.0", - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@streamdown/cjk": { - "optional": true - }, - "@streamdown/code": { - "optional": true - }, - "@streamdown/math": { - "optional": true - }, - "@streamdown/mermaid": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/store": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/@assistant-ui/store/-/store-0.2.13.tgz", - "integrity": "sha512-7NL6HWMBxe1ndLWO4kHkjQ0Syyc0D/Aj+zxdpcy4yrplG71X04CzFimMBBSQAk+AnGBf+d96D7cuUZdjHkTavg==", - "license": "MIT", - "dependencies": { - "use-effect-event": "^2.0.3" - }, - "peerDependencies": { - "@assistant-ui/tap": "^0.5.14", - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@assistant-ui/tap": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@assistant-ui/tap/-/tap-0.5.16.tgz", - "integrity": "sha512-6f3RxJdE+5NCndmf8i8SJYq7C5qzrH4olyOw3Nzer7pLy4uB6ZYkV2fi2UR7W44NIxfg7ur9UCT56krjZKXrSw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/@audiowave/core": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@audiowave/core/-/core-0.3.1.tgz", @@ -1656,679 +1655,171 @@ "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, "engines": { - "node": ">=18" + "node": ">=16.4" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=14.14" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -8435,15 +7926,6 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -15552,36 +15034,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16898,6 +16350,12 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-content-frame": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/safe-content-frame/-/safe-content-frame-0.0.21.tgz", + "integrity": "sha512-4LgYcX0lESOg9zVi6QCcqHaNGjZuc86w0IGRJij7lA4YG/6QHBblL2Jwh5OJBtkVSnhDOeARRpD3jvFNGiIWiw==", + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", diff --git a/package.json b/package.json index 9e9448497f68..b5e5c59af84f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ }, "overrides": { "lodash": "4.18.1", - "@assistant-ui/store": "0.2.13", "yauzl": "^3.3.1" }, "engines": { From 6a88a080357363bc90071ed25bf4a5aff87aae93 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Wed, 24 Jun 2026 11:57:48 +0800 Subject: [PATCH 2/8] feat: replace custom streaming wrappers with built-in defer and smooth props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delete SmoothStreamingText, DeferStreamingText, and useSmoothReveal (~174 lines) from markdown-text.tsx. the built-in defer and smooth props on StreamdownTextPrimitive now handle the same work: - defer: routes streaming text through useDeferredValue so markdown re-parsing runs at lower priority (typing/scrolling stay responsive) - smooth: typewriter-style reveal via useSmooth with SmoothOptions { drainMs: 500, maxCharsPerFrame: 30, minCommitMs: 33 }, matching the old useSmoothReveal constants exactly MarkdownTextContent (reasoning text) gets both defer and smooth. MarkdownText (assistant text) gets defer only, matching the previous behavior where text messages had no typewriter effect. the internal pipeline order changes from smooth → defer → preprocess to preprocess → smooth → defer (the built-in primitive runs preprocess first). this is functionally equivalent: the tail-bounded remend repair runs once on the full text instead of per revealed prefix, and the smooth reveal operates on already-repaired markdown. end result is identical. verified: tsc 0 errors, eslint clean, vitest 0 new failures (15 pre-existing, 792 passing), manual verification of 6 streaming scenarios (defer, smooth reveal, typing-while-streaming, code blocks, math, long text performance). --- .../components/assistant-ui/markdown-text.tsx | 194 ++---------------- 1 file changed, 20 insertions(+), 174 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index beabcbf8cc27..0ecbd5621678 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -1,6 +1,6 @@ 'use client' -import { TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react' +import { type SmoothOptions, TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react' import { parseMarkdownIntoBlocks, type StreamdownTextComponents, @@ -8,16 +8,7 @@ import { type SyntaxHighlighterProps } from '@assistant-ui/react-streamdown' import { code } from '@streamdown/code' -import { - type ComponentProps, - memo, - type ReactNode, - useDeferredValue, - useEffect, - useMemo, - useRef, - useState -} from 'react' +import { type ComponentProps, memo, useEffect, useMemo, useState } from 'react' import { ExpandableBlock } from '@/components/chat/expandable-block' import { PreviewAttachment } from '@/components/chat/preview-attachment' @@ -71,7 +62,7 @@ function preprocessWithTailRepair(text: string): string { // `marked` lex of the entire message, ~1.6ms per 28KB) inside a useMemo keyed // on the text — but the same text is re-lexed every time a message REMOUNTS // (virtualizer scroll, session switch) and whenever multiple surfaces render -// the same content (deferred + smooth reveal republish). A small module-level +// the same content (remount, session switch). A small module-level // LRU keyed by the exact source string removes all of those repeat parses // with zero correctness risk (same input → same output). Streaming tail // growth misses the cache by design (every flush is a new string) — that @@ -306,150 +297,11 @@ function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>) ) } -// Steady character-reveal for streaming text: decouples visible cadence from -// bursty arrival so text flows instead of popping (cf. assistant-ui's useSmooth, -// reimplemented for a tunable rate). Proportional drain — each frame reveals a -// slice of the backlog so the reveal converges within ~REVEAL_DRAIN_MS whatever -// the size; the per-frame cap stops a huge dump rendering as one slab. The loop -// is gated on backlog, not isRunning, so a stream that completes mid-reveal -// keeps draining its tail instead of snapping. -const REVEAL_DRAIN_MS = 500 -const REVEAL_MAX_CHARS_PER_FRAME = 30 -// Floor between reveal commits. Each commit republishes the text context and -// re-runs the whole Streamdown pipeline (preprocess → remend → lex → micromark -// on the open block) over the full accumulated text — at raw rAF cadence -// that's 60 full parses/second and was the dominant streaming cost for -// reasoning text. ~33ms keeps the reveal visually fluid (2 frames) while -// halving the parse work. -const REVEAL_MIN_COMMIT_MS = 33 - -function useSmoothReveal(text: string, isRunning: boolean): string { - const [displayed, setDisplayed] = useState(isRunning ? '' : text) - const targetRef = useRef(text) - const shownRef = useRef(displayed) - const frameRef = useRef(null) - const lastTickRef = useRef(0) - - shownRef.current = displayed - targetRef.current = text - - useEffect(() => { - if (typeof window === 'undefined') { - return - } - - // Non-extending change (regenerate / branch / history swap): restart from - // empty while streaming, else snap to the replacement. - if (!text.startsWith(shownRef.current)) { - shownRef.current = isRunning ? '' : text - setDisplayed(shownRef.current) - } - - if (shownRef.current.length >= text.length || frameRef.current !== null) { - return - } - - lastTickRef.current = performance.now() - - const tick = () => { - const now = performance.now() - const dt = now - lastTickRef.current - - // Skip this frame if the floor hasn't elapsed — the backlog math below - // is dt-proportional, so delayed commits reveal proportionally more. - if (dt < REVEAL_MIN_COMMIT_MS) { - frameRef.current = requestAnimationFrame(tick) - - return - } - - lastTickRef.current = now - - const remaining = targetRef.current.length - shownRef.current.length - - const add = Math.min( - remaining, - // dt-scaled so the per-commit cap stays equivalent to the old - // per-frame cap at any commit cadence. - Math.ceil((REVEAL_MAX_CHARS_PER_FRAME * dt) / 16.7), - Math.max(1, Math.ceil((remaining * dt) / REVEAL_DRAIN_MS)) - ) - - shownRef.current = targetRef.current.slice(0, shownRef.current.length + add) - setDisplayed(shownRef.current) - - frameRef.current = shownRef.current.length < targetRef.current.length ? requestAnimationFrame(tick) : null - } - - frameRef.current = requestAnimationFrame(tick) - }, [text, isRunning]) - - useEffect( - () => () => { - if (frameRef.current !== null && typeof window !== 'undefined') { - cancelAnimationFrame(frameRef.current) - } - }, - [] - ) - - return displayed -} - -// Re-publish the part context with a smooth character-reveal, above -// DeferStreamingText so the reveal feeds the deferred markdown pipeline. Status -// stays running while revealing so the caret persists past the underlying part -// settling. -function SmoothStreamingText({ children }: { children: ReactNode }) { - const { text, status } = useMessagePartText() - const isRunning = status.type === 'running' - const revealed = useSmoothReveal(text, isRunning) - - return ( - - {children} - - ) -} - -/** - * Re-publish the active message-part context with React's `useDeferredValue` - * applied to the streaming text and status. The outer wrapper still re-renders - * on every token, but the work it does is trivial (one hook, one provider). - * - * The expensive subtree (Streamdown → micromark → mdast → hast → React) lives - * inside `` and reads the deferred text via the - * normal `useMessagePartText` hook. React's concurrent scheduler then has - * permission to: - * - skip intermediate token states when the next token arrives mid-render - * (it abandons the in-flight deferred render and starts over) - * - deprioritize the markdown render when the main thread is busy with an - * urgent task (typing, scrolling, layout work elsewhere) - * - * Net effect: per-token CPU is unchanged but the *blocking* part of that work - * goes away — typing-while-streaming stays a single-frame paint, scroll - * stutter disappears, and the longtask histogram tightens because long - * commits can be interrupted and discarded. - * - * Industry standard (Streamdown's own block-array setState already uses - * `useTransition`); this just lifts the deferral up to the consumer text - * boundary so it covers the whole pipeline, not just the inner setState. - */ -function DeferStreamingText({ children }: { children: ReactNode }) { - const { text, status } = useMessagePartText() - const deferredText = useDeferredValue(text) - const isRunning = status.type === 'running' - - return ( - - {children} - - ) -} - interface MarkdownTextSurfaceProps { containerClassName?: string containerProps?: ComponentProps<'div'> + defer?: boolean + smooth?: boolean | SmoothOptions } // Headings shrink to chat scale rather than the prose default (h1≈xl). Kept @@ -498,7 +350,7 @@ function HugeTextFallback({ containerClassName, text }: { containerClassName?: s ) } -function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTextSurfaceProps) { +function MarkdownTextSurface({ containerClassName, containerProps, defer, smooth }: MarkdownTextSurfaceProps) { const { status, text } = useMessagePartText() const isStreaming = status.type === 'running' @@ -626,26 +478,28 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex components={components} containerClassName={cn(MARKDOWN_CONTAINER_CLASS_NAME, containerClassName)} containerProps={containerProps} + defer={defer} lineNumbers={false} mode="streaming" - // Incomplete-markdown repair is handled by `preprocessWithTailRepair` - // below (tail-bounded remend) instead of Streamdown's built-in pass, - // which re-runs remend over the ENTIRE message on every flush — ~18% - // of streaming script time on 50KB+ messages. The repair itself stays - // always-on (even between flushes / for completed messages): an - // unclosed ```python ... ``` whose body contains `$` (shell snippets, - // JS template strings, dollar amounts) would otherwise leak those - // dollars to the math parser and render broken inline math. Shiki is - // independently deferred via `defer={isStreaming}` on the - // SyntaxHighlighter component. + // The built-in tail-bounded remend is disabled when a custom + // parseMarkdownIntoBlocksFn is supplied, so repair runs in + // preprocessWithTailRepair instead. parseIncompleteMarkdown stays + // false to avoid a second full-text remend pass. parseIncompleteMarkdown={false} parseMarkdownIntoBlocksFn={parseMarkdownIntoBlocksCached} plugins={plugins} preprocess={preprocessWithTailRepair} + smooth={smooth} /> ) } +const SMOOTH_OPTIONS: SmoothOptions = { + drainMs: 500, + maxCharsPerFrame: 30, + minCommitMs: 33 +} + interface MarkdownTextContentProps extends MarkdownTextSurfaceProps { isRunning: boolean text: string @@ -654,21 +508,13 @@ interface MarkdownTextContentProps extends MarkdownTextSurfaceProps { export function MarkdownTextContent({ isRunning, text, ...surfaceProps }: MarkdownTextContentProps) { return ( - - - - - + ) } const MarkdownTextImpl = () => { - return ( - - - - ) + return } export const MarkdownText = memo(MarkdownTextImpl) From 0d92a9990d130866913f183d04172df897e5b0f2 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Wed, 24 Jun 2026 12:05:03 +0800 Subject: [PATCH 3/8] refactor: replace custom lib/remend-tail.ts with built-in tailBoundedRemend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delete lib/remend-tail.ts (108 lines) and lib/remend-tail.test.ts (105 lines). the tailBoundedRemend export from @assistant-ui/react-streamdown 0.3.4 is algorithmically identical — same findRemendWindowStart boundary scan, same fence/math tracking, same slice-and-repair strategy. the only differences are improvements: the built-in handles \r (CR) in line endings for Windows compatibility, and accepts an optional RemendOptions parameter passed through to remend. the import in markdown-text.tsx moves from @/lib/remend-tail to @assistant-ui/react-streamdown. the call site (preprocessWithTailRepair) is unchanged. verified: tsc 0 errors, eslint clean, vitest 0 new failures (15 pre-existing, 786 passing — 6 fewer than before because the deleted remend-tail.test.ts had 6 cases), manual verification of incomplete markdown repair during streaming. --- .../components/assistant-ui/markdown-text.tsx | 8 +- apps/desktop/src/lib/remend-tail.test.ts | 105 ----------------- apps/desktop/src/lib/remend-tail.ts | 108 ------------------ 3 files changed, 4 insertions(+), 217 deletions(-) delete mode 100644 apps/desktop/src/lib/remend-tail.test.ts delete mode 100644 apps/desktop/src/lib/remend-tail.ts diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index 0ecbd5621678..6837c27b9b8a 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -5,7 +5,8 @@ import { parseMarkdownIntoBlocks, type StreamdownTextComponents, StreamdownTextPrimitive, - type SyntaxHighlighterProps + type SyntaxHighlighterProps, + tailBoundedRemend } from '@assistant-ui/react-streamdown' import { code } from '@streamdown/code' import { type ComponentProps, memo, useEffect, useMemo, useState } from 'react' @@ -28,7 +29,6 @@ import { mediaStreamUrl } from '@/lib/media' import { previewTargetFromMarkdownHref } from '@/lib/preview-targets' -import { tailBoundedRemend } from '@/lib/remend-tail' import { cn } from '@/lib/utils' import { detectEmbed, extractAlert, MarkdownAlert, RichCodeBlock, UrlEmbed } from './embeds' @@ -48,8 +48,8 @@ import { detectEmbed, extractAlert, MarkdownAlert, RichCodeBlock, UrlEmbed } fro const mathPlugin = createMemoizedMathPlugin({ singleDollarTextMath: true }) // Replaces Streamdown's `parseIncompleteMarkdown` (full-text remend per -// flush) with a tail-bounded repair — see lib/remend-tail.ts. Must stay -// module-scope so the prop identity is stable across renders. +// flush) with a tail-bounded repair. Must stay module-scope so the prop +// identity is stable across renders. function preprocessWithTailRepair(text: string): string { try { return tailBoundedRemend(preprocessMarkdown(text)) diff --git a/apps/desktop/src/lib/remend-tail.test.ts b/apps/desktop/src/lib/remend-tail.test.ts deleted file mode 100644 index c730937356d5..000000000000 --- a/apps/desktop/src/lib/remend-tail.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { parseMarkdownIntoBlocks } from '@assistant-ui/react-streamdown' -import remend from 'remend' -import { describe, expect, it } from 'vitest' - -import { findRemendWindowStart, tailBoundedRemend } from './remend-tail' - -const CORPUS = `# Heading one - -Intro paragraph with **bold**, *italic*, \`inline code\`, and a [link](https://example.com). - -## Code - -\`\`\`python -def main(): - cost = "$5" - print(f"total: $\{cost}") -\`\`\` - -Some text after the fence with $x^2 + y^2$ inline math. - -$$ -\\int_0^1 f(x) dx -$$ - -- list item one with **bold** -- list item two - -| col a | col b | -| ----- | ----- | -| 1 | 2 | - -~~~js -const s = \`template \${value}\` -~~~ - -Final paragraph with ~~strike~~ and unfinished [link text](https://exa -` - -/** - * Render-equivalence oracle: full-text remend and tail-bounded remend may - * differ in raw string output ONLY in ways that cannot affect rendering — - * i.e. after block splitting, every block must be identical. (Streamdown - * renders blocks independently, so block-level equality IS render equality.) - */ -function blocksOf(text: string): string[] { - return parseMarkdownIntoBlocks(text) -} - -describe('tailBoundedRemend', () => { - it('matches full remend block output at every streaming prefix', () => { - for (let end = 1; end <= CORPUS.length; end++) { - const prefix = CORPUS.slice(0, end) - const full = blocksOf(remend(prefix)) - const tail = blocksOf(tailBoundedRemend(prefix)) - - expect(tail, `prefix length ${end}: ${JSON.stringify(prefix.slice(-60))}`).toEqual(full) - } - }) - - it('repairs an unclosed fence opened early in a long message', () => { - const text = `intro\n\n\`\`\`python\n${'x = 1\n'.repeat(500)}print("$dollar")` - const repaired = tailBoundedRemend(text) - - expect(blocksOf(repaired)).toEqual(blocksOf(remend(text))) - // the window must reach back to the fence opener - expect(findRemendWindowStart(text)).toBe(text.indexOf('```python')) - }) - - it('bounds the window to the tail paragraph when no fence is open', () => { - const text = `para one\n\npara two\n\npara three with **bold` - const start = findRemendWindowStart(text) - - expect(start).toBe(text.indexOf('para three')) - expect(tailBoundedRemend(text)).toBe(remend(text)) - }) - - it('widens the window across an open $$ math block', () => { - const text = `before\n\n$$\n\\frac{a}{b}` - const start = findRemendWindowStart(text) - - expect(start).toBeLessThanOrEqual(text.indexOf('$$')) - expect(blocksOf(tailBoundedRemend(text))).toEqual(blocksOf(remend(text))) - }) - - it('handles closed constructs without modification', () => { - const text = `done **bold** and \`code\`\n\n\`\`\`js\nconst a = 1\n\`\`\`\n\nlast line.` - - expect(tailBoundedRemend(text)).toBe(text) - }) - - it('intentionally diverges from full remend on cross-block dangling openers', () => { - // Full remend scans the whole document and appends `**` for an opener - // left dangling in an EARLIER block, dumping stray asterisks into the - // unrelated tail block ("|**"). Because Streamdown splits into blocks - // after the repair, that opener never renders as bold either way — the - // tail-bounded result is the cleaner of the two. This test documents - // the divergence so a future remend upgrade that changes the behavior - // gets noticed. - const text = `- item with **dangling\n- item two\n\n|` - - expect(remend(text).endsWith('|**')).toBe(true) - expect(tailBoundedRemend(text).endsWith('|')).toBe(true) - expect(tailBoundedRemend(text).endsWith('|**')).toBe(false) - }) -}) diff --git a/apps/desktop/src/lib/remend-tail.ts b/apps/desktop/src/lib/remend-tail.ts deleted file mode 100644 index 683f7dc193e0..000000000000 --- a/apps/desktop/src/lib/remend-tail.ts +++ /dev/null @@ -1,108 +0,0 @@ -import remend from 'remend' - -// Tail-bounded incomplete-markdown repair. -// -// Streamdown's built-in `parseIncompleteMarkdown` runs `remend` over the whole -// accumulated message on every streaming flush (~18% of script time on 50KB+ -// messages). But repairs only ever matter in the trailing block: inline -// constructs can't cross a blank line, and Streamdown splits into blocks AFTER -// the repair, so a dangling opener in an earlier block can't reach the tail. -// We run `remend` on just that block instead. - -const BACKTICK = 96 // ` -const TILDE = 126 // ~ -const SPACE = 32 -const TAB = 9 -const BACKSLASH = 92 - -const isSpace = (c: number) => c === SPACE || c === TAB - -/** - * Index of the last top-level block start — the char after the most recent - * blank line that sits outside any open code fence or `$$` math block. An - * unclosed fence/math always begins after that blank, so it stays wholly - * inside the window without separate tracking. One cheap char pass, no regex. - */ -export function findRemendWindowStart(text: string): number { - const n = text.length - let inFence = false - let fenceChar = 0 - let fenceRun = 0 - let inMath = false - let boundary = 0 - let pending = -1 // a blank line, committed to `boundary` once content follows - - for (let lineStart = 0; lineStart <= n; ) { - let lineEnd = text.indexOf('\n', lineStart) - - if (lineEnd === -1) { - lineEnd = n - } - - let i = lineStart - - while (i < lineEnd && isSpace(text.charCodeAt(i))) { - i += 1 - } - - const first = i < lineEnd ? text.charCodeAt(i) : -1 - let marker = false - - // Fence open/close (``` or ~~~, ≤3 spaces indent). - if ((first === BACKTICK || first === TILDE) && i - lineStart <= 3) { - let run = i - - while (run < lineEnd && text.charCodeAt(run) === first) { - run += 1 - } - - if (run - i >= 3) { - marker = true - - if (!inFence) { - inFence = true - fenceChar = first - fenceRun = run - i - } else if (first === fenceChar && run - i >= fenceRun && onlyWhitespace(text, run, lineEnd)) { - inFence = false - } - } - } - - // Toggle `$$` math state on plain lines ($$ inside a fence is literal). - if (!inFence && !marker) { - for (let s = text.indexOf('$$', lineStart); s !== -1 && s < lineEnd - 1; s = text.indexOf('$$', s + 2)) { - if (s === 0 || text.charCodeAt(s - 1) !== BACKSLASH) { - inMath = !inMath - } - } - } - - if (first === -1 && !inFence && !inMath) { - pending = lineEnd + 1 - } else if (pending !== -1) { - boundary = pending - pending = -1 - } - - lineStart = lineEnd + 1 - } - - return boundary -} - -function onlyWhitespace(text: string, from: number, to: number): boolean { - for (let i = from; i < to; i += 1) { - if (!isSpace(text.charCodeAt(i))) { - return false - } - } - - return true -} - -export function tailBoundedRemend(text: string): string { - const start = findRemendWindowStart(text) - - return start <= 0 ? remend(text) : text.slice(0, start) + remend(text.slice(start)) -} From cab0f69b46d2da3ba4e0951fc791d7a6480f5491 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Wed, 24 Jun 2026 12:11:28 +0800 Subject: [PATCH 4/8] refactor: replace custom math delimiter helpers with built-in normalizeMathDelimiters and escapeCurrencyDollars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delete the custom rewriteLatexBracketDelimiters and escapeCurrencyDollars implementations from markdown-preprocess.ts (~40 lines). the built-in exports from @assistant-ui/react-streamdown 0.3.4 are strict improvements: - normalizeMathDelimiters combines rewriteLatexBracketDelimiters (now handles double backslashes and trims body whitespace) with rewriteCustomMathTags (handles [/math]...[/math] and [/inline]...[/inline] tags that some models emit — new capability HA didn't have before) - escapeCurrencyDollars excludes $ as a preceding character, so display math $$5 is no longer incorrectly escaped (bugfix) the call site in preprocessMarkdown changes from rewriteLatexBracketDelimiters(escapeCurrencyDollars(part)) to normalizeMathDelimiters(escapeCurrencyDollars(part)). verified: tsc 0 errors, eslint clean, all 16 preprocessMarkdown tests pass (including currency dollar escaping), vitest 0 new failures, manual verification of currency amounts, LaTeX bracket delimiters, display math, and dollar signs inside code blocks. --- apps/desktop/src/lib/markdown-preprocess.ts | 47 +++------------------ 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/lib/markdown-preprocess.ts b/apps/desktop/src/lib/markdown-preprocess.ts index 5fd08453b26e..6137a7d61e2d 100644 --- a/apps/desktop/src/lib/markdown-preprocess.ts +++ b/apps/desktop/src/lib/markdown-preprocess.ts @@ -1,3 +1,5 @@ +import { escapeCurrencyDollars, normalizeMathDelimiters } from '@assistant-ui/react-streamdown' + import { isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code' import { stripPreviewTargets } from '@/lib/preview-targets' @@ -310,41 +312,6 @@ function normalizeFenceBlocks(text: string): string { return out.join('\n') } -// Convert LaTeX bracket delimiters to remark-math's dollar-sign syntax. -// Models often emit `\(...\)` for inline math and `\[...\]` for display -// math (the standard LaTeX convention) instead of `$...$` / `$$...$$`. -// remark-math only natively recognizes the dollar form, so we rewrite at -// preprocess time. Done with simple non-greedy matches keyed on the -// escaped-bracket sequences — these are rare enough in non-math content -// (you'd have to write a literal `\(` followed eventually by a literal -// `\)` with NO interleaving newline-paragraph-break) that false positives -// are extremely unlikely. -const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g -const LATEX_DISPLAY_RE = /\\\[([\s\S]+?)\\\]/g - -function rewriteLatexBracketDelimiters(text: string): string { - return text - .replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`) - .replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`) -} - -// Escape `$` patterns so they don't get eaten as math delimiters. -// Models commonly write currency amounts ($5, $19.99, $1,299) in prose. -// With `singleDollarTextMath: true`, remark-math is greedy and matches -// EVERY pair of `$`s — including the open of `$5` to the next `$10`, -// rendering "5 in my pocket and you have " as italicized math text. -// The de-facto convention across math-supporting LLM UIs is to treat -// `$` followed by a digit as currency rather than math, since math -// expressions almost always start with a letter or `\command`. Trade- -// off: a math expression like `$5x = 10$` would have its leading 5 -// escaped — annoying but rare. The escape `\$` survives to render as -// a literal `$` in the final output. -const CURRENCY_DOLLAR_RE = /(^|[^\\])\$(?=\d)/g - -function escapeCurrencyDollars(text: string): string { - return text.replace(CURRENCY_DOLLAR_RE, '$1\\$') -} - export function preprocessMarkdown(text: string): string { const cleaned = text.replace(REASONING_BLOCK_RE, '').replace(PREVIEW_MARKER_RE, '') const scrubbed = scrubBacktickNoise(cleaned) @@ -377,12 +344,12 @@ export function preprocessMarkdown(text: string): string { const leading = part.match(/^\s*/)?.[0] ?? '' const trailing = part.match(/\s*$/)?.[0] ?? '' - // rewriteLatexBracketDelimiters runs only on prose segments so - // we don't accidentally touch `\(` inside a code block. - // escapeCurrencyDollars likewise only runs on prose, so legit - // `$5` literals inside fenced code stay intact. + // normalizeMathDelimiters rewrites LaTeX bracket delimiters and + // custom math tags to dollar form. escapeCurrencyDollars escapes + // `$` so currency isn't eaten as math. Both run only on + // prose segments so code blocks stay untouched. const transformed = normalizeVisibleProse( - stripPreviewTargets(rewriteLatexBracketDelimiters(escapeCurrencyDollars(part))) + stripPreviewTargets(normalizeMathDelimiters(escapeCurrencyDollars(part))) ) return leading + transformed + trailing From 24a940fa356d4bcb40e1cdd1cabdeefc3ab7957f Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 30 Jun 2026 17:24:24 +0800 Subject: [PATCH 5/8] fix(desktop): match renamed useClientLookup out-of-bounds throw in MessageRenderBoundary @assistant-ui/store renamed its index-out-of-bounds throw from tapClientLookup/tapClientResource to useClientLookup in the 0.14 upgrade, so the boundary's /tapClient.../ filter stopped matching and re-threw the transient session-switch and reconnect race to root, blanking the app. broaden the regex to accept the new prefix (keeping the old one for older store versions) and point the test at the real message so it exercises the live path instead of the dead string. --- .../components/assistant-ui/message-render-boundary.test.tsx | 4 ++-- .../src/components/assistant-ui/message-render-boundary.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx b/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx index 934f3babd1c0..8e1b70f7934f 100644 --- a/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +++ b/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx @@ -13,7 +13,7 @@ function Boom({ error }: { error: Error | null }): null { return null } -const lookupError = new Error('tapClientLookup: Index 2 out of bounds (length: 2)') +const lookupError = new Error('useClientLookup: Index 2 out of bounds (length: 2)') describe('MessageRenderBoundary', () => { it('renders children when nothing throws', () => { @@ -26,7 +26,7 @@ describe('MessageRenderBoundary', () => { expect(screen.getByText('content')).toBeTruthy() }) - it('swallows the transient tapClientLookup out-of-bounds store race', () => { + it('swallows the transient useClientLookup out-of-bounds store race', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined) const { container } = render( diff --git a/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx b/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx index 990f8c6072e4..f0fafbc4f18f 100644 --- a/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +++ b/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx @@ -1,6 +1,6 @@ import { Component, type ReactNode } from 'react' -// `@assistant-ui/store`'s index-keyed child-scope lookup (`tapClientLookup`) +// `@assistant-ui/store`'s index-keyed child-scope lookup (`useClientLookup`) // throws — rather than returning undefined — when a subscriber reads an index // that the message/parts list no longer has. This races during high-frequency // store replacement (session switch mid-stream, gateway reconnect replay): a @@ -10,7 +10,7 @@ import { Component, type ReactNode } from 'react' // without a local boundary it unwinds to the root and blanks the whole app. // Upstream-tracked: assistant-ui/assistant-ui#4051, #3652. const isTransientLookupError = (error: unknown): boolean => - error instanceof Error && /tapClient(Lookup|Resource).*out of bounds/.test(error.message) + error instanceof Error && /(useClientLookup|tapClient(Lookup|Resource)).*out of bounds/.test(error.message) interface Props { // Changes whenever the message list mutates; remounting clears the caught From fbe98caa9099b37dd0c66557b9df3e4923d26bab Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 30 Jun 2026 17:24:58 +0800 Subject: [PATCH 6/8] docs(desktop): correct streaming-repair comments after the defer/smooth swap the parseIncompleteMarkdown comment implied the reveal frontier is repaired; repair runs on the full accumulated text, so reword it to say that. drop the now-dead "multiple surfaces render the same content" clause from the block-cache comment (the smooth and defer wrappers that caused it were removed), and trim the math-preprocess comment to the load-bearing prose-only constraint. --- .../src/components/assistant-ui/markdown-text.tsx | 12 ++++++------ apps/desktop/src/lib/markdown-preprocess.ts | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index 6837c27b9b8a..259c87ba1c5f 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -61,8 +61,7 @@ function preprocessWithTailRepair(text: string): string { // Memoized block splitter. Streamdown calls `parseMarkdownIntoBlocks` (a full // `marked` lex of the entire message, ~1.6ms per 28KB) inside a useMemo keyed // on the text — but the same text is re-lexed every time a message REMOUNTS -// (virtualizer scroll, session switch) and whenever multiple surfaces render -// the same content (remount, session switch). A small module-level +// (virtualizer scroll, session switch). A small module-level // LRU keyed by the exact source string removes all of those repeat parses // with zero correctness risk (same input → same output). Streaming tail // growth misses the cache by design (every flush is a new string) — that @@ -481,10 +480,11 @@ function MarkdownTextSurface({ containerClassName, containerProps, defer, smooth defer={defer} lineNumbers={false} mode="streaming" - // The built-in tail-bounded remend is disabled when a custom - // parseMarkdownIntoBlocksFn is supplied, so repair runs in - // preprocessWithTailRepair instead. parseIncompleteMarkdown stays - // false to avoid a second full-text remend pass. + // Incomplete-markdown repair runs in preprocessWithTailRepair on the + // full accumulated text; the built-in tail-bounded remend is disabled + // because a custom parseMarkdownIntoBlocksFn is supplied, and + // parseIncompleteMarkdown stays false to avoid a second full-text + // remend pass. parseIncompleteMarkdown={false} parseMarkdownIntoBlocksFn={parseMarkdownIntoBlocksCached} plugins={plugins} diff --git a/apps/desktop/src/lib/markdown-preprocess.ts b/apps/desktop/src/lib/markdown-preprocess.ts index 6137a7d61e2d..63768a710d14 100644 --- a/apps/desktop/src/lib/markdown-preprocess.ts +++ b/apps/desktop/src/lib/markdown-preprocess.ts @@ -344,10 +344,8 @@ export function preprocessMarkdown(text: string): string { const leading = part.match(/^\s*/)?.[0] ?? '' const trailing = part.match(/\s*$/)?.[0] ?? '' - // normalizeMathDelimiters rewrites LaTeX bracket delimiters and - // custom math tags to dollar form. escapeCurrencyDollars escapes - // `$` so currency isn't eaten as math. Both run only on - // prose segments so code blocks stay untouched. + // Run only on prose segments so `$5` literals and `\(` inside code + // blocks stay intact. const transformed = normalizeVisibleProse( stripPreviewTargets(normalizeMathDelimiters(escapeCurrencyDollars(part))) ) From 94756bd7a935a8c7a621e1106f4e1ab6ce4b3ad3 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 30 Jun 2026 17:38:44 +0800 Subject: [PATCH 7/8] test(desktop): add math-delimiter fixtures for the react-streamdown preprocess swap lock the four behaviors the built-in normalizeMathDelimiters/escapeCurrencyDollars introduce over the deleted custom helpers: $$$$ display math stays intact, double-backslash brackets and [/math]/[/inline] tag pairs rewrite to dollar delimiters, and currency dollars in prose are escaped. the existing preprocessMarkdown suite had no math cases. --- .../assistant-ui/markdown-text.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts index b3ea416d0664..748bef09e288 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +++ b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts @@ -210,4 +210,29 @@ describe('preprocessMarkdown', () => { expect(() => preprocessMarkdown(input)).not.toThrow() }) + + it('keeps $$$$ display math intact instead of escaping it as currency', () => { + const output = preprocessMarkdown('$$5x = 10$$') + + expect(output).toContain('$$5x = 10$$') + expect(output).not.toContain('\\$') + }) + + it('rewrites double-backslash bracket math to dollar delimiters', () => { + const output = preprocessMarkdown('\\\\(x^2\\\\)') + + expect(output).toContain('$x^2$') + }) + + it('rewrites [/math] and [/inline] tag pairs to dollar delimiters', () => { + expect(preprocessMarkdown('[/math]a+b[/math]')).toContain('$$a+b$$') + expect(preprocessMarkdown('[/inline]x[/inline]')).toContain('$x$') + }) + + it('escapes currency dollars in prose so they are not parsed as math', () => { + const output = preprocessMarkdown('$5 and $10') + + expect(output).toContain('\\$5') + expect(output).toContain('\\$10') + }) }) From 27f8d78a521421e76b9f5948e1b8e14340d8283f Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 30 Jun 2026 17:50:53 +0800 Subject: [PATCH 8/8] docs(desktop): note the load-bearing isOptimistic invariant on the optimistic placeholder a reader of this subclass can't recover from hermes code alone that the metadata.isOptimistic flag drives core's off-branch eviction and export() omission, so a future core change to it would silently break placeholder cleanup. flagged in the upgrade review. --- apps/desktop/src/lib/incremental-external-store-runtime.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/src/lib/incremental-external-store-runtime.ts b/apps/desktop/src/lib/incremental-external-store-runtime.ts index e8d2033c4dbd..48c0c424dfd2 100644 --- a/apps/desktop/src/lib/incremental-external-store-runtime.ts +++ b/apps/desktop/src/lib/incremental-external-store-runtime.ts @@ -136,6 +136,8 @@ class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRunti self._notifyEventSubscribers(store.isRunning ? 'runStart' : 'runEnd', {}) } + // metadata.isOptimistic keeps this placeholder ephemeral: core evicts + // off-branch optimistic messages on head moves and omits them from export(). if (hasUpcomingMessage(isRunning, messages)) { const optimisticId = generateId() this.repository.addOrUpdateMessage(