From 809bf5af54ad5ca0bd74443de3628b94f1655a82 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Wed, 12 Aug 2026 11:25:31 +0530 Subject: [PATCH 01/43] fix(preview): an ogcapture URL is parsed, not sliced, so Windows finds its images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A URL's authority comes before its path, so `ogcapture://C:/Users/oga/…` puts the drive letter in the HOST and drops its colon. Slicing the scheme off the string therefore produced `C/Users/oga/…`, which names nothing - every generated-image preview 404'd on Windows while the file sat on disk and Download worked. macOS never showed it: its paths start with a slash, the host is empty, and the remainder is already absolute. The rule is pure and lives beside the other path boundary in this file, so both dialects can be proved without a protocol handler or a running app. --- src/main/index.ts | 6 ++++-- src/main/ogcapture-serve.ts | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 929bfe4c..1e107b9e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -24,7 +24,7 @@ import { preloadPath } from './preload-path' import { rendererHtmlPath } from './renderer-path' import { startModelServer, stopModelServer } from './model-server' import { startMediaServer, stopMediaServer, mediaUrlFor } from './media-server' -import { serveCaptureFile } from './ogcapture-serve' +import { capturePathFromUrl, serveCaptureFile } from './ogcapture-serve' import { serveArtifactPreview } from './artifact-preview' import { ipcMain } from 'electron' import { loadProEntitlementProvider, loadProFeaturesMain } from './bootstrap/loadProFeaturesMain' @@ -299,7 +299,9 @@ app.whenReady().then(async () => { const ogCaptureRoots = localMediaRoots(app.getPath('userData')) protocol.handle('ogcapture', async (request) => { try { - const requestedPath = decodeURIComponent(request.url.slice('ogcapture://'.length)) + // Parsed, not sliced: a Windows drive letter lands in the URL's host and loses its colon, so + // slicing produced `C/Users/…` and every preview 404'd on that platform alone. + const requestedPath = capturePathFromUrl(request.url) return serveCaptureFile(requestedPath, ogCaptureRoots, request.headers.get('Range')) } catch { return new Response(null, { status: 400 }) diff --git a/src/main/ogcapture-serve.ts b/src/main/ogcapture-serve.ts index dc3748bd..64fe70e4 100644 --- a/src/main/ogcapture-serve.ts +++ b/src/main/ogcapture-serve.ts @@ -122,3 +122,28 @@ export async function serveCaptureFile( return new Response(null, { status: 404 }) } } + +/** + * The local path an `ogcapture://` URL names, on either platform. + * + * Slicing the prefix off the URL string is what broke Windows. A URL is parsed, not cut: the authority + * comes before the path, so `ogcapture://C:/Users/oga/x.png` puts the drive letter in the HOST - and the + * colon is dropped there - leaving `C/Users/oga/x.png`, which names nothing. macOS never showed it + * because its paths start with a slash, so the host is empty and the remainder is already absolute. The + * image generated, the file was on disk, and only the preview could not find it. + * + * Pure, so both dialects can be proved without a protocol handler or a running app. + */ +export function capturePathFromUrl(url: string): string { + const withoutScheme = url.replace(/^[a-z]+:\/\//i, ''); + const separator = withoutScheme.indexOf('/'); + const authority = separator === -1 ? withoutScheme : withoutScheme.slice(0, separator); + const rest = separator === -1 ? '' : withoutScheme.slice(separator); + // A single-letter authority is a Windows drive, and the colon it lost belongs back on it. Anything + // longer is a real host, which this scheme never has, so it is treated as part of the path. + const decodedAuthority = decodeURIComponent(authority); + const decodedRest = decodeURIComponent(rest); + if (/^[a-zA-Z]$/.test(decodedAuthority)) return `${decodedAuthority}:${decodedRest}`; + if (decodedAuthority === '') return decodedRest; + return `${decodedAuthority}${decodedRest}`; +} From 69f5c13da33018bfff4a3a6b62879fb234a7b153 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Wed, 12 Aug 2026 11:25:31 +0530 Subject: [PATCH 02/43] test(preview): the drive letter, the POSIX path, and the escaping in between Uses the real failing path from the report, including the space in "Off Grid AI Desktop", and pins that a longer authority is never mistaken for a drive. --- src/main/__tests__/ogcapture-path.test.ts | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/main/__tests__/ogcapture-path.test.ts diff --git a/src/main/__tests__/ogcapture-path.test.ts b/src/main/__tests__/ogcapture-path.test.ts new file mode 100644 index 00000000..df145bef --- /dev/null +++ b/src/main/__tests__/ogcapture-path.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { capturePathFromUrl } from '../ogcapture-serve' + +/** + * The Windows preview defect, held down where it can be proved without a protocol handler. + * + * An image generated on Windows, the file was on disk, download worked, and only the preview broke. The + * handler sliced the scheme off the URL string - but a URL is parsed, not cut. The authority comes before + * the path, so a drive letter lands in the host and loses its colon. + */ +describe('capturePathFromUrl', () => { + it('keeps a Windows drive letter, colon and all', () => { + // What the old slice produced: 'C/Users/oga/AppData/Roaming/Off Grid AI Desktop/generated-images/a.png' + expect( + capturePathFromUrl( + 'ogcapture://C:/Users/oga/AppData/Roaming/Off Grid AI Desktop/generated-images/a.png' + ) + ).toBe('C:/Users/oga/AppData/Roaming/Off Grid AI Desktop/generated-images/a.png') + }) + + it('leaves a POSIX path exactly as it was, which is why macOS never saw the fault', () => { + expect(capturePathFromUrl('ogcapture:///Users/user/Library/generated-images/a.png')).toBe( + '/Users/user/Library/generated-images/a.png' + ) + }) + + it('decodes the escaping a real path needs', () => { + expect(capturePathFromUrl('ogcapture:///Users/user/Off%20Grid/a%20b.png')).toBe( + '/Users/user/Off Grid/a b.png' + ) + expect(capturePathFromUrl('ogcapture://C%3A/Users/oga/a%20b.png')).toBe( + 'C:/Users/oga/a b.png' + ) + }) + + it('treats a longer authority as path, since this scheme has no host', () => { + // Never invents a drive out of something that is not one letter. + expect(capturePathFromUrl('ogcapture://relative/a.png')).toBe('relative/a.png') + }) +}) From 3046ac9ca0d5eaffd07c0598918bedf56d5abb3a Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Wed, 12 Aug 2026 13:12:17 +0530 Subject: [PATCH 03/43] chore(llama): one owner for the engine version, moved to b10369 The version was hardcoded in the macOS source build and the Windows binary fetch, and passed AGAIN by two callers - four homes for one fact. The macOS engine and the Windows binaries have to be the same llama.cpp or grammar and native tool-call handling differ between the platforms of a single release, which is the exact drift the pinning test exists to catch. It could not catch a version, only a string. Now `scripts/llama-ref.txt` owns it, both scripts read it, and neither caller overrides it. The test asserts that too: no LLAMA_REF may appear in the release workflow or the local build. b10369 (2026-08-12) is what the move buys: it knows `muse-glimmer`, `nemotron`, `nemotron_h` and `nemotron_h_moe`, so Muse Glimmer 30B and Nemotron 3.5 can both load on desktop. Muse Glimmer is a desktop model by Meta's own numbers - under 20 GB at 4-bit, needing a 24-32 GB envelope. --- .github/workflows/release.yml | 2 +- docs/WINDOWS_SUPPORT.md | 2 +- scripts/build-llama.sh | 8 ++++++-- scripts/build-mac-local.sh | 2 +- scripts/fetch-win-binaries.ps1 | 9 ++++++--- scripts/llama-ref.txt | 1 + src/main/__tests__/whisper-cli-build.integration.test.ts | 8 +++++++- 7 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 scripts/llama-ref.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0f4f188..4946667c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,7 +125,7 @@ jobs: - name: Build llama-server (pinned deployment target) run: | command -v cmake >/dev/null || brew install cmake - MACOS_DEPLOYMENT_TARGET=13.0 LLAMA_REF=b9838 bash scripts/build-llama.sh + MACOS_DEPLOYMENT_TARGET=13.0 bash scripts/build-llama.sh otool -l resources/bin/llama/llama-server | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print "[ci] bundled engine minos="$2; exit}' # Rebuild the one-shot Whisper CLI instead of trusting the committed LFS # payload. The production script disables OpenMP/BLAS auto-discovery and diff --git a/docs/WINDOWS_SUPPORT.md b/docs/WINDOWS_SUPPORT.md index b2c2dd26..3cf044f3 100644 --- a/docs/WINDOWS_SUPPORT.md +++ b/docs/WINDOWS_SUPPORT.md @@ -31,7 +31,7 @@ at the bottom. | -------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Windows CI build | 🟒 | `.github/workflows/windows-build.yml` (`windows-2022`) builds + packages a branch artifact; `release.yml`'s `build-win` job publishes the installer + updater feed to the release. Verified by installing a build on real Windows hardware. | | Native-module compile (node-gyp) | 🟒 | Pinned toolchain: `windows-2022` (VS 2022) + Python 3.12. `windows-latest`/VS 2026 + Python 3.13 break node-gyp 11 β€” documented in the workflow. Covers `better-sqlite3-multiple-ciphers`, `node-llama-cpp`, `sharp`. | -| Windows runtime binaries fetch | 🟒 | `scripts/fetch-win-binaries.ps1` pulls win64 `llama-server` / `whisper-cli` / `sd-cli` / `ffmpeg` (+ DLLs) from upstream GitHub releases at build time. **`llama-server` is pinned to `b9838`** (byte-for-byte parity with the macOS engine); `whisper-cli` / `sd-cli` / `ffmpeg` resolve dynamically from their latest upstream releases. Repo LFS binaries are macOS-only and skipped (`lfs: false`). Fails loud if `llama-server.exe` is missing. | +| Windows runtime binaries fetch | 🟒 | `scripts/fetch-win-binaries.ps1` pulls win64 `llama-server` / `whisper-cli` / `sd-cli` / `ffmpeg` (+ DLLs) from upstream GitHub releases at build time. **`llama-server` is pinned by `scripts/llama-ref.txt`** β€” one owner, shared with the macOS source build, so the two are byte-for-byte parity; `whisper-cli` / `sd-cli` / `ffmpeg` resolve dynamically from their latest upstream releases. Repo LFS binaries are macOS-only and skipped (`lfs: false`). Fails loud if `llama-server.exe` is missing. | | NSIS installer | 🟒 | `electron-builder.yml` β†’ `win.executableName`, `nsis` block (desktop shortcut, uninstall name). Untested end-to-end. | | Code signing | 🟑 | Optional via `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` secrets; **unset β†’ unsigned build β†’ SmartScreen will warn** on install. No cert configured yet. | | Auto-update | 🟒 | `electron-updater` is cross-platform (`src/main/updater.ts`); `release.yml`'s `build-win` job publishes `latest.yml` (stable) / `beta.yml` (nightly) to the release, so Windows installs self-update like macOS. | diff --git a/scripts/build-llama.sh b/scripts/build-llama.sh index 46e2e7f2..728f75f6 100755 --- a/scripts/build-llama.sh +++ b/scripts/build-llama.sh @@ -11,9 +11,13 @@ set -euo pipefail # official llama.cpp release binaries are now minos 26. The only reliable fix is # to build it ourselves with the target pinned. Run in CI before packaging. # -# LLAMA_REF=b9838 MACOS_DEPLOYMENT_TARGET=13.0 scripts/build-llama.sh +# LLAMA_REF=b10369 MACOS_DEPLOYMENT_TARGET=13.0 scripts/build-llama.sh (override; default in llama-ref.txt) -LLAMA_REF="${LLAMA_REF:-b9838}" # gemma4/qwen35-capable build +# ONE owner for the version, read by this script and by fetch-win-binaries.ps1. It was hardcoded in both, +# which is a single fact with two homes: the macOS source build and the Windows binary fetch have to be the +# same llama.cpp or grammar and tool-call handling differ between the platforms of one release. +LLAMA_REF_FILE="$(cd "$(dirname "$0")" && pwd)/llama-ref.txt" +LLAMA_REF="${LLAMA_REF:-$(tr -d '[:space:]' < "$LLAMA_REF_FILE")}" TARGET="${MACOS_DEPLOYMENT_TARGET:-13.0}" # runs on macOS 13+ ROOT="${OFFGRID_BUILD_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" DEST="$ROOT/resources/bin/llama" diff --git a/scripts/build-mac-local.sh b/scripts/build-mac-local.sh index dc3e71a9..780cde89 100755 --- a/scripts/build-mac-local.sh +++ b/scripts/build-mac-local.sh @@ -52,7 +52,7 @@ stage_native_helpers() { echo "!! cmake is required to rebuild llama-server and Whisper. Install it before continuing." exit 1 } - MACOS_DEPLOYMENT_TARGET=13.0 LLAMA_REF=b9838 bash scripts/build-llama.sh + MACOS_DEPLOYMENT_TARGET=13.0 bash scripts/build-llama.sh MACOS_DEPLOYMENT_TARGET=13.0 WHISPER_REF=v1.7.4 bash scripts/build-whisper-cli.sh bash scripts/build-meeting-recorder.sh bash scripts/build-dictation-hotkey.sh diff --git a/scripts/fetch-win-binaries.ps1 b/scripts/fetch-win-binaries.ps1 index 9feaed80..82767646 100644 --- a/scripts/fetch-win-binaries.ps1 +++ b/scripts/fetch-win-binaries.ps1 @@ -14,7 +14,7 @@ # Most runtimes are resolved DYNAMICALLY from each project's latest GitHub # release so the script does not go stale. llama.cpp is the EXCEPTION: it is # pinned to the same ref the macOS engine is built from (scripts/build-llama.sh, -# LLAMA_REF=b9838) so grammar / native tool-call handling is byte-for-byte +# scripts/llama-ref.txt) so grammar / native tool-call handling is byte-for-byte # identical across platforms. 'latest' floats, and upstream builds have shipped # that reject the tool-call GBNF the app generates from MCP tool schemas. # Set OFFGRID_GH_TOKEN (or GITHUB_TOKEN) to avoid the unauthenticated API rate @@ -41,7 +41,7 @@ if ($token) { $ghHeaders['Authorization'] = "Bearer $token" } # Find the download URL of a release asset whose name matches $pattern. With no # $tag it uses the project's LATEST release; with $tag it pins to that exact -# release (e.g. llama.cpp b9838, to match the macOS source build). +# release (the ref in scripts/llama-ref.txt, to match the macOS source build). function Get-AssetUrl($repo, $pattern, $tag) { $uri = if ($tag) { "https://api.github.com/repos/$repo/releases/tags/$tag" } else { "https://api.github.com/repos/$repo/releases/latest" } @@ -86,7 +86,10 @@ function Copy-Runtime($srcDir, $destName) { # bin/llama-cpu <- CPU-only build, the app's FALLBACK (llm.ts) for the rare # box with no Vulkan loader at all, where the Vulkan .exe # can't even load. -$LlamaRef = if ($env:LLAMA_REF) { $env:LLAMA_REF } else { 'b9838' } +# The version has ONE owner: scripts/llama-ref.txt, shared with build-llama.sh. Hardcoding it in both is +# how the macOS build and the Windows binaries drift apart within a single release. +$LlamaRefFile = Join-Path $PSScriptRoot 'llama-ref.txt' +$LlamaRef = if ($env:LLAMA_REF) { $env:LLAMA_REF } else { (Get-Content $LlamaRefFile -Raw).Trim() } Write-Host "== llama.cpp (pinned $LlamaRef): vulkan primary + cpu fallback ==" try { $x = Expand-Asset 'ggml-org/llama.cpp' 'bin-win-vulkan-x64\.zip$' $LlamaRef diff --git a/scripts/llama-ref.txt b/scripts/llama-ref.txt new file mode 100644 index 00000000..ea2157fc --- /dev/null +++ b/scripts/llama-ref.txt @@ -0,0 +1 @@ +b10369 diff --git a/src/main/__tests__/whisper-cli-build.integration.test.ts b/src/main/__tests__/whisper-cli-build.integration.test.ts index d63168b9..839baef7 100644 --- a/src/main/__tests__/whisper-cli-build.integration.test.ts +++ b/src/main/__tests__/whisper-cli-build.integration.test.ts @@ -191,7 +191,10 @@ describe('pinned Whisper CLI build and staging', () => { it('keeps release and local builds on the same pinned native-engine scripts', () => { const release = fs.readFileSync(path.join(REPO_ROOT, '.github/workflows/release.yml'), 'utf8') const local = fs.readFileSync(path.join(REPO_ROOT, 'scripts/build-mac-local.sh'), 'utf8') - const llamaBuild = 'MACOS_DEPLOYMENT_TARGET=13.0 LLAMA_REF=b9838 bash scripts/build-llama.sh' + // No LLAMA_REF here on purpose. The version has one owner - scripts/llama-ref.txt - and a caller that + // passes its own would silently build a different engine than the Windows fetch pulls, which is the + // drift this test exists to catch. + const llamaBuild = 'MACOS_DEPLOYMENT_TARGET=13.0 bash scripts/build-llama.sh' const whisperBuild = 'MACOS_DEPLOYMENT_TARGET=13.0 WHISPER_REF=v1.7.4 bash scripts/build-whisper-cli.sh' @@ -199,6 +202,9 @@ describe('pinned Whisper CLI build and staging', () => { expect(source).toContain(llamaBuild) expect(source).toContain(whisperBuild) } + for (const source of [release, local]) { + expect(source).not.toMatch(/LLAMA_REF=/) + } expect(local).toContain('bash scripts/fetch-parakeet.sh') expect(local).toContain('node scripts/probe-packaged-helpers.mjs "$app_dir"') expect(local.match(/^\s+verify_packaged_helpers$/gm)).toHaveLength(2) From 5e165a8d9ddc4b99993a1352fc0e96ba3fb02c0a Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Wed, 12 Aug 2026 16:57:28 +0530 Subject: [PATCH 04/43] fix(sidebar): a row's hover colour comes from the text token, not the surface one neutral-900 is remapped onto --og-surface in this app, so hover:text-neutral-900 painted the label #f5f5f5 on a #eaeaea row - invisible in light mode, and only in light mode, because the dark: variant is Tailwind's prefers-color-scheme and rescued it whenever the OS was dark. The dark: variants go with it: the palette already flips on data-theme, so a second theme source could only ever disagree with the first. One navRowClass now colours every sidebar row - nav items, the model-status row, the mobile-app link - which also collapses four copies of the same class string. The divider above the bottom nav had the same bug (neutral-200 is the TEXT token, drawing a hard black rule in light mode). --- src/renderer/src/App.tsx | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index eaee738f..0ca17169 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -154,6 +154,22 @@ function ReprocessingBanner() { ) } +// One rule for the look of EVERY sidebar row - nav items, the model-status row, the mobile-app +// link. The Tailwind palette is remapped onto the theme-aware --og-* tokens in assets/main.css, +// so these classes already flip with data-theme and no `dark:` variant belongs here: `dark:` is +// Tailwind's own prefers-color-scheme media query, a SECOND source of truth for the theme that +// disagrees with data-theme whenever the app theme and the OS theme differ. +// The tell that made this visible: neutral-900 is a SURFACE token here (#f5f5f5 in light), not a +// text token, so `hover:text-neutral-900` painted the label near-white on a near-white row. +const navRowClass = (expanded: boolean, active = false): string => + cn( + 'group/nav relative flex items-center gap-3 rounded-lg py-2 text-sm transition-colors', + expanded ? 'px-3' : 'justify-center px-0', + active + ? 'bg-green-500/10 text-emerald-400' + : 'text-neutral-400 hover:bg-neutral-500/10 hover:text-white' + ) + // Model-server health dot for the sidebar. Uses the SAME live probe as the System // Health panel (system:health β†’ real /health check), not llm.isReady() (an internal // flag that lags). Green = running, amber = starting, red = stopped (e.g. a SIGKILL @@ -214,15 +230,7 @@ function ModelStatusDot({ : `Model server: ${text.toLowerCase()}` : `${text} - expand for details` return ( - @@ -820,13 +828,7 @@ function AppContent() { key={item.view} onClick={() => goToView(item.view)} title={!sidebarOpen ? item.label : undefined} - className={cn( - 'group/nav relative flex items-center gap-3 rounded-lg py-2 text-sm transition-colors', - sidebarOpen ? 'px-3' : 'justify-center px-0', - active - ? 'bg-green-500/10 text-green-600 dark:text-emerald-400' - : 'text-neutral-500 hover:bg-neutral-500/10 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white' - )} + className={navRowClass(sidebarOpen, active)} > {active && ( @@ -1012,7 +1014,9 @@ function AppContent() { {/* Pinned bottom */} -
+ {/* neutral-800 is the surface token, theme-aware on its own - neutral-200 is the TEXT + token, which drew a hard black rule here in light mode. See navRowClass. */} +
{ @@ -1032,11 +1036,7 @@ function AppContent() {
)} -
) } @@ -1074,7 +1093,7 @@ export function ModelsScreen(): React.JSX.Element { ['Source', m.org || (isLocal ? 'Imported' : 'β€”')], ['Parameters', m.params ? `${m.params}B` : null], ['Quantization', m.quant || null], - ['Download', bytes > 0 ? `${(bytes / 1e9).toFixed(1)} GB` : null], + ['Download', formatSize(bytes) || null], ['Released', fmtReleaseDate(m.releaseDate) || null], ['Min RAM', m.minRamGb ? `${m.minRamGb} GB` : null] ] diff --git a/src/renderer/src/components/__tests__/ModelsScreen.download-states.integration.test.tsx b/src/renderer/src/components/__tests__/ModelsScreen.download-states.integration.test.tsx index 0250d1cf..08740965 100644 --- a/src/renderer/src/components/__tests__/ModelsScreen.download-states.integration.test.tsx +++ b/src/renderer/src/components/__tests__/ModelsScreen.download-states.integration.test.tsx @@ -172,7 +172,12 @@ describe(' β€” what a download looks like', () => { // One percent for the whole download. expect(await screen.findByText(/20%/)).toBeTruthy() // Bytes at the scale the card above already uses β€” 6296.4 MB is a number you have to convert. - expect(screen.getByText(/1\.3 GB of 6\.1 GB/)).toBeTruthy() + // + // The feed counts MEBIbytes, so 6296.4 is 6.6 GB, not 6.1. This line used to assert 6.1 because + // it divided by 1024 while the meta line above it divided by 1e9 β€” one file, two units, one + // label, which is what made a 25.4GB model report "23.7 GB" while downloading. Both now read + // through formatSize, so this assertion finally matches the intent stated above it. + expect(screen.getByText(/1\.4 GB of 6\.6 GB/)).toBeTruthy() // Which part is moving, without giving it a second percent of its own. expect(screen.getByText(/file 1 of 2/)).toBeTruthy() // The action row holds the action, on the same line as the status. From f04edbb3cda8a24d88363f3c885c63d81263b378 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 13 Aug 2026 09:58:52 +0530 Subject: [PATCH 12/43] fix(imagegen): a generated image previews on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderer built a URL by pasting a path after the scheme. On macOS every path starts with `/`, so the authority came out empty and the rest was already the path. On Windows `C:\Users\…` has no slash at all, so the whole thing landed in the AUTHORITY, the backslashes made it an invalid host, and Chromium rejected the URL outright - the request was never made, so nothing logged a 403 or a 404. Only previews with a data-URL fallback still drew, which is why a thumbnail could render while the full-size view beside it was broken. Writer and reader now live together in `shared/`, because main and renderer each own one half, and a scheme written apart is a scheme that works on one platform only. Both are pure, so both dialects can be proved without a protocol handler or a window. --- src/main/ogcapture-serve.ts | 26 +++-------------- src/shared/ogcapture-url.ts | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 src/shared/ogcapture-url.ts diff --git a/src/main/ogcapture-serve.ts b/src/main/ogcapture-serve.ts index 64fe70e4..daddc799 100644 --- a/src/main/ogcapture-serve.ts +++ b/src/main/ogcapture-serve.ts @@ -124,26 +124,8 @@ export async function serveCaptureFile( } /** - * The local path an `ogcapture://` URL names, on either platform. - * - * Slicing the prefix off the URL string is what broke Windows. A URL is parsed, not cut: the authority - * comes before the path, so `ogcapture://C:/Users/oga/x.png` puts the drive letter in the HOST - and the - * colon is dropped there - leaving `C/Users/oga/x.png`, which names nothing. macOS never showed it - * because its paths start with a slash, so the host is empty and the remainder is already absolute. The - * image generated, the file was on disk, and only the preview could not find it. - * - * Pure, so both dialects can be proved without a protocol handler or a running app. + * The reader half of the scheme now lives beside its writer in `shared/ogcapture-url`, because the + * renderer builds these URLs and the main process resolves them: written apart, they drifted, and the + * drift was invisible on macOS. Re-exported here so existing callers and their tests keep their import. */ -export function capturePathFromUrl(url: string): string { - const withoutScheme = url.replace(/^[a-z]+:\/\//i, ''); - const separator = withoutScheme.indexOf('/'); - const authority = separator === -1 ? withoutScheme : withoutScheme.slice(0, separator); - const rest = separator === -1 ? '' : withoutScheme.slice(separator); - // A single-letter authority is a Windows drive, and the colon it lost belongs back on it. Anything - // longer is a real host, which this scheme never has, so it is treated as part of the path. - const decodedAuthority = decodeURIComponent(authority); - const decodedRest = decodeURIComponent(rest); - if (/^[a-zA-Z]$/.test(decodedAuthority)) return `${decodedAuthority}:${decodedRest}`; - if (decodedAuthority === '') return decodedRest; - return `${decodedAuthority}${decodedRest}`; -} +export { capturePathFromUrl } from '../shared/ogcapture-url' diff --git a/src/shared/ogcapture-url.ts b/src/shared/ogcapture-url.ts new file mode 100644 index 00000000..78f62f93 --- /dev/null +++ b/src/shared/ogcapture-url.ts @@ -0,0 +1,56 @@ +/** + * The two halves of the `ogcapture://` scheme: a local path becomes a URL, and that URL becomes the + * path again. They live together, and in `shared/`, because main and renderer each own one half β€” and + * a scheme whose writer and reader are written apart is a scheme that works on one platform only. + * + * What went wrong without this: the renderer built a URL by pasting a path after the scheme. On macOS + * every path starts with `/`, so the authority came out empty and the rest was already the path. On + * Windows the path starts `C:\Users\…`, which has no `/` at all β€” so the whole thing landed in the + * authority, the backslashes made it an invalid host, and Chromium rejected the URL outright. The + * image never reached the protocol handler, so nothing logged a 403 or a 404: the request was never + * made. Only the previews with a `preview` data-URL fallback still drew, which is why a thumbnail + * could render while the full-size view beside it was broken. + * + * Both functions are pure, so both dialects can be proved without a protocol handler or a window. + */ + +/** The URL that names `absolutePath`, valid on both platforms. + * + * The drive letter goes in the PATH behind a leading slash, never in the authority: this scheme has + * no host, and a one-letter host is a coincidence that a real URL parser is entitled to normalise. + * Every segment is percent-encoded, so a space, a `#` or a `?` in a filename survives the round trip. + */ +export function captureUrlForPath(absolutePath: string): string { + if (!absolutePath) return '' + const forwardSlashed = absolutePath.replace(/\\/g, '/') + const rooted = forwardSlashed.startsWith('/') ? forwardSlashed : `/${forwardSlashed}` + const encoded = rooted.split('/').map(encodeURIComponent).join('/') + return `ogcapture://${encoded}` +} + +/** + * The local path an `ogcapture://` URL names, on either platform. + * + * Accepts every form the scheme has ever carried, because a URL that is already on screen must keep + * resolving: the rooted form this module writes (`ogcapture:///C%3A/…`), the drive-in-authority form + * (`ogcapture://C:/…`), and a plain POSIX path. + */ +export function capturePathFromUrl(url: string): string { + const withoutScheme = url.replace(/^[a-z]+:\/\//i, '') + const separator = withoutScheme.indexOf('/') + const authority = separator === -1 ? withoutScheme : withoutScheme.slice(0, separator) + const rest = separator === -1 ? '' : withoutScheme.slice(separator) + // A single-letter authority is a Windows drive, and the colon it lost belongs back on it. Anything + // longer is a real host, which this scheme never has, so it is treated as part of the path. + const decodedAuthority = decodeURIComponent(authority) + const decodedRest = decodeURIComponent(rest) + if (/^[a-zA-Z]$/.test(decodedAuthority)) return `${decodedAuthority}:${decodedRest}` + if (decodedAuthority === '') return stripRootBeforeDriveLetter(decodedRest) + return `${decodedAuthority}${decodedRest}` +} + +/** `/C:/Users/x` is how a Windows path rides in a URL path; `C:/Users/x` is how the filesystem + * wants it. A POSIX path is left exactly as it was, which is why macOS never saw this fault. */ +function stripRootBeforeDriveLetter(pathname: string): string { + return /^\/[a-zA-Z]:/.test(pathname) ? pathname.slice(1) : pathname +} From 511fd2dbbdeaf15ef01fbdaf0b13a36eb916d5b1 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 13 Aug 2026 09:58:52 +0530 Subject: [PATCH 13/43] fix(files): every attachment failed on Windows, and sharp took the blame sharp ships libvips as `libvips-42.dll`, and Windows resolves a DLL by NAME across the whole process: the first copy loaded wins every later binding. Three sharp versions were in the tree, and `embeddings.ts` loads @xenova/transformers (sharp 0.32 / libvips 8.14.5) at startup - so our sharp 0.35 asked that older DLL for symbols it does not export and died with ERR_DLOPEN_FAILED. Proven by load order: sharp alone loads; @xenova-then-sharp does not. macOS binds by path, so three copies coexist and it never showed there. `overrides` holds the tree at one sharp, so that clash cannot recur. The second half is worse: the upload path imported sharp at the TOP LEVEL, so a module that only validates images took every attachment down with it - a PDF and a text file cannot be attached either, and neither has anything to do with sharp. Loading is now on demand and the answer is three-valued: only a READ verdict refuses a file. A validator that will not load is our fault, not a statement about the user's photo. --- package-lock.json | 1058 ++++----------------------------- package.json | 4 + src/main/files-image-probe.ts | 74 +++ src/main/files.ts | 13 +- 4 files changed, 211 insertions(+), 938 deletions(-) create mode 100644 src/main/files-image-probe.ts diff --git a/package-lock.json b/package-lock.json index 31ad8bd9..afc7f097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1927,510 +1927,6 @@ "node": ">=18" } }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@huggingface/transformers/node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@huggingface/transformers/node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2518,62 +2014,6 @@ "node": ">=12.0.0" } }, - "node_modules/@huggingface/transformers/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@huggingface/transformers/node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, "node_modules/@huggingface/transformers/node_modules/tar": { "version": "7.5.16", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", @@ -2661,9 +2101,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", - "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -2679,13 +2119,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.1" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", - "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -2701,20 +2141,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.1" + "@img/sharp-libvips-darwin-x64": "1.3.2" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", - "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.3" }, "engines": { "node": ">=20.9.0" @@ -2724,9 +2164,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", - "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -2740,9 +2180,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", - "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -2756,9 +2196,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", - "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -2775,9 +2215,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", - "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -2794,9 +2234,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", - "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -2813,9 +2253,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", - "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -2832,9 +2272,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", - "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -2851,9 +2291,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", - "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -2870,9 +2310,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", - "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -2889,9 +2329,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", - "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -2908,9 +2348,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", - "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -2929,13 +2369,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.1" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", - "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -2954,13 +2394,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.1" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", - "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -2979,13 +2419,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.1" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", - "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -3004,13 +2444,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.1" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", - "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -3029,13 +2469,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.1" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", - "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -3054,13 +2494,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.1" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", - "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -3079,13 +2519,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", - "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -3104,13 +2544,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", - "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3124,16 +2564,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", - "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.3" }, "engines": { "node": ">=20.9.0" @@ -3143,9 +2583,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", - "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -3162,9 +2602,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", - "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -3181,9 +2621,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", - "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -7660,73 +7100,6 @@ "onnxruntime-node": "1.14.0" } }, - "node_modules/@xenova/transformers/node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "license": "MIT" - }, - "node_modules/@xenova/transformers/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@xenova/transformers/node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@xenova/transformers/node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@xenova/transformers/node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -8552,20 +7925,6 @@ "dev": true, "license": "MIT" }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -8583,98 +7942,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", - "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", - "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", - "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", - "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", - "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9347,19 +8614,6 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9378,16 +8632,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -11244,15 +10488,6 @@ "node": ">= 0.6" } }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -11425,12 +10660,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -13050,12 +12279,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -18122,14 +17345,14 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", - "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "semver": "^7.8.5" }, "engines": { "node": ">=20.9.0" @@ -18138,31 +17361,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.2", - "@img/sharp-darwin-x64": "0.35.2", - "@img/sharp-freebsd-wasm32": "0.35.2", - "@img/sharp-libvips-darwin-arm64": "1.3.1", - "@img/sharp-libvips-darwin-x64": "1.3.1", - "@img/sharp-libvips-linux-arm": "1.3.1", - "@img/sharp-libvips-linux-arm64": "1.3.1", - "@img/sharp-libvips-linux-ppc64": "1.3.1", - "@img/sharp-libvips-linux-riscv64": "1.3.1", - "@img/sharp-libvips-linux-s390x": "1.3.1", - "@img/sharp-libvips-linux-x64": "1.3.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", - "@img/sharp-libvips-linuxmusl-x64": "1.3.1", - "@img/sharp-linux-arm": "0.35.2", - "@img/sharp-linux-arm64": "0.35.2", - "@img/sharp-linux-ppc64": "0.35.2", - "@img/sharp-linux-riscv64": "0.35.2", - "@img/sharp-linux-s390x": "0.35.2", - "@img/sharp-linux-x64": "0.35.2", - "@img/sharp-linuxmusl-arm64": "0.35.2", - "@img/sharp-linuxmusl-x64": "0.35.2", - "@img/sharp-webcontainers-wasm32": "0.35.2", - "@img/sharp-win32-arm64": "0.35.2", - "@img/sharp-win32-ia32": "0.35.2", - "@img/sharp-win32-x64": "0.35.2" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/sharp/node_modules/semver": { @@ -18329,15 +17557,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -18518,17 +17737,6 @@ "node": ">= 0.4" } }, - "node_modules/streamx": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", - "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -18982,15 +18190,6 @@ "license": "ISC", "optional": true }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -19069,15 +18268,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json index bee6dbe7..c1216937 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,10 @@ "knip": "knip --no-gitignore", "coverage:all": "bash scripts/coverage-all.sh" }, + "//overrides": "ONE sharp for the whole tree. sharp ships libvips as `libvips-42.dll`, and Windows resolves a DLL by NAME across the process: whichever copy loads first wins for every later binding. embeddings.ts loads @xenova/transformers (sharp 0.32 / libvips 8.14.5) at startup, so our own sharp 0.35 (libvips 8.18.3) then asked that older DLL for symbols it does not export and died with ERR_DLOPEN_FAILED. Every file attachment failed on Windows and only there β€” macOS binds by path, so three copies coexist. Proven by load order: sharp alone loads; @xenova-then-sharp does not. Deduping to one version is the fix, not load-order juggling.", + "overrides": { + "sharp": "^0.35.2" + }, "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/src/main/files-image-probe.ts b/src/main/files-image-probe.ts new file mode 100644 index 00000000..63b31d29 --- /dev/null +++ b/src/main/files-image-probe.ts @@ -0,0 +1,74 @@ +// Is an image's BYTES decodable? Asked so that the machinery which answers can +// never become a precondition for attaching a file. +// +// sharp is a native module, and it can fail to load for reasons that have nothing +// to do with the file in front of it. sharp ships libvips as `libvips-42.dll`, and +// Windows resolves a DLL by NAME across the whole process: the first copy loaded +// wins every later binding. A second sharp version anywhere in the tree therefore +// broke ours with ERR_DLOPEN_FAILED β€” `embeddings.ts` loads @xenova/transformers +// (sharp 0.32 / libvips 8.14.5) at startup, so our sharp 0.35 asked that older DLL +// for symbols it does not export. macOS binds by path and never showed it. +// +// `package.json` overrides now hold the tree at one sharp, so that specific clash +// cannot recur. The reason this module exists is the SECOND half of that failure: +// the upload path imported sharp at the top level, so a module that only validates +// images took every attachment down with it. A PDF and a text file cannot be +// attached on Windows either, and neither has anything to do with sharp. +// +// Hence a three-valued answer. "I could not check" is not "this file is broken". + +/** The one call this check needs from sharp β€” injected so the decision can be + * proved against a real file without a working native module. */ +export interface ImageProbe { + (filePath: string): { metadata(): Promise } +} + +export type ImageDecodeVerdict = + /** sharp read the header: the bytes are a real image. */ + | 'decodable' + /** sharp read the file and rejected it: the user's file is damaged. Say so. */ + | 'undecodable' + /** sharp itself did not load: the check is SKIPPED, not failed. Attach anyway. */ + | 'unchecked' + +/** Load sharp on demand. Returns null when the module cannot load at all, so the + * caller can distinguish a missing checker from a bad file. */ +export async function loadImageProbe(): Promise { + try { + const mod = (await import('sharp')) as unknown as { + default?: (p: string, o?: unknown) => { metadata(): Promise } + } + const sharp = mod.default ?? (mod as unknown as typeof mod.default) + if (!sharp) return null + // failOn: 'error' β€” a truncated or corrupt image must reject rather than decode + // to garbage and reach the vision runtime as engine noise. + return (filePath: string) => sharp(filePath, { failOn: 'error' }) + } catch (e) { + // Deliberately not thrown: an unloadable validator is an infrastructure fault, + // and it must not present itself as a verdict on the user's file. + console.warn( + '[files] image validation unavailable (sharp did not load); attaching without it:', + (e as Error).message.split('\n')[0] + ) + return null + } +} + +/** + * Judge a file's bytes. Pure in its decision: the probe is injected, so the three + * outcomes are provable without depending on whether this machine's native module + * happens to work. + */ +export async function verifyImageDecodable( + filePath: string, + load: () => Promise = loadImageProbe +): Promise { + const probe = await load() + if (!probe) return 'unchecked' + try { + await probe(filePath).metadata() + return 'decodable' + } catch { + return 'undecodable' + } +} diff --git a/src/main/files.ts b/src/main/files.ts index 9a408b21..818e2fe2 100644 --- a/src/main/files.ts +++ b/src/main/files.ts @@ -8,9 +8,12 @@ import path from 'path' import os from 'os' import fs from 'fs' import { app } from 'electron' -import sharp from 'sharp' import { desktopExtraction as ex } from './rag/extractors' import { IMAGE_EXT, AUDIO_EXT, VIDEO_EXT, sanitizeUploadName } from './files-classify' +// sharp is NOT imported here. It is a native module, and a top-level import let a +// module that only validates images refuse every attachment of every type when it +// could not load β€” which is exactly what happened on Windows. See files-image-probe. +import { verifyImageDecodable } from './files-image-probe' export interface ProcessedFile { name: string @@ -33,9 +36,11 @@ export async function processUpload( // the upload owner before persisting or marking the attachment ready, so a // damaged image produces a specific recoverable error in the composer instead // of reaching the vision runtime as engine garbage. - try { - await sharp(tmp, { failOn: 'error' }).metadata() - } catch { + // + // Only a READ verdict refuses the file. If the validator itself is unavailable + // the upload proceeds unchecked: a native module that will not load is our + // fault, not a statement about the user's photo. + if ((await verifyImageDecodable(tmp)) === 'undecodable') { throw new Error('Unsupported or damaged image data.') } // Persist the image so the chat can pass the ACTUAL image to the multimodal From 33bb2a017eca304b9081d79fdab3a9a6ee844170 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 13 Aug 2026 09:59:07 +0530 Subject: [PATCH 14/43] fix(chat): a streamed reply is stored under the id its peers already know A reply is named at its first token, so the frames a paired device renders live carry that id. Minting a fresh one when the record was stored left the peer unable to match the two, so it drew the answer twice until its preview timed out. The id is read from the one owner of "what this device is generating", so no caller has to pass it and none can forget to. --- src/main/__tests__/chat-stream-state.test.ts | 32 ++++++++++--- src/main/chat-stream-state.ts | 48 ++++++++++++++++++-- src/main/database.ts | 12 ++++- src/main/ipc.ts | 10 +++- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/main/__tests__/chat-stream-state.test.ts b/src/main/__tests__/chat-stream-state.test.ts index 6366dce3..1b0d67b4 100644 --- a/src/main/__tests__/chat-stream-state.test.ts +++ b/src/main/__tests__/chat-stream-state.test.ts @@ -17,7 +17,13 @@ import { * Nothing is faked. The hook registry is the app's own, and the only collaborator. */ -type Snapshot = { conversationId: string; content: string; reasoning: string } | null +type Snapshot = { + conversationId: string + content: string + reasoning: string + /** Minted when the turn is bound, so the record that follows keeps the id its frames carried. */ + messageId?: string +} | null describe('the reply being generated, as published to anything that follows it', () => { let published: Snapshot[] @@ -38,7 +44,14 @@ describe('the reply being generated, as published to anything that follows it', // Empty content, not absent: a consumer can show "generating" immediately, rather than waiting for // the first token to learn a turn exists at all. - expect(published).toEqual([{ conversationId: 'conversation-1', content: '', reasoning: '' }]) + expect(published).toEqual([ + { + conversationId: 'conversation-1', + content: '', + reasoning: '', + messageId: expect.any(String) + } + ]) }) it('publishes the reply so far, not the delta, so a late consumer is never behind', () => { @@ -51,7 +64,8 @@ describe('the reply being generated, as published to anything that follows it', expect(published.at(-1)).toEqual({ conversationId: 'conversation-1', content: 'Hello world', - reasoning: '' + reasoning: '', + messageId: expect.any(String) }) }) @@ -66,7 +80,8 @@ describe('the reply being generated, as published to anything that follows it', expect(published.at(-1)).toEqual({ conversationId: 'conversation-1', content: 'the answer', - reasoning: 'let me think β€” more thought' + reasoning: 'let me think β€” more thought', + messageId: expect.any(String) }) }) @@ -116,12 +131,14 @@ describe('the reply being generated, as published to anything that follows it', expect(published.at(-2)).toEqual({ conversationId: 'conversation-1', content: 'first', - reasoning: '' + reasoning: '', + messageId: expect.any(String) }) expect(published.at(-1)).toEqual({ conversationId: 'conversation-2', content: 'second', - reasoning: '' + reasoning: '', + messageId: expect.any(String) }) endChatStream('stream-b') expect(published.at(-1)).toBeNull() @@ -131,7 +148,8 @@ describe('the reply being generated, as published to anything that follows it', expect(published.at(-1)).toEqual({ conversationId: 'conversation-1', content: 'first more', - reasoning: '' + reasoning: '', + messageId: expect.any(String) }) }) diff --git a/src/main/chat-stream-state.ts b/src/main/chat-stream-state.ts index 6bd293c7..c894a4d8 100644 --- a/src/main/chat-stream-state.ts +++ b/src/main/chat-stream-state.ts @@ -16,23 +16,64 @@ interface ActiveStream { conversationId: string content: string reasoning: string + /** + * The id this reply will be STORED under, when the caller named it before the first token. + * + * Published with every snapshot, so a paired device's live preview and the durable record that + * follows share one identity - which is what lets the preview be retired the moment the record + * lands instead of standing beside it until it times out. + */ + messageId?: string } const active = new Map() /** - * Attach a conversation to a stream id, before any delta arrives. + * The identity minted for a conversation's current reply, until its stored record claims it. + * + * Deliberately OUTLIVES the stream. The turn ends in this process the moment the model stops, while + * the record is written afterwards by the renderer over IPC - so an identity discarded on `end` is + * always gone before the thing that needs it asks. Keyed by conversation because chat generation is + * serialised through one queue: a conversation has at most one reply forming at a time. + */ +const pendingMessageIds = new Map() + +/** + * Attach a conversation to a stream id, before any delta arrives, and name the reply it will become. * * Deltas are keyed by stream id because that is all the streaming transport knows; the conversation * is known only by the handler that started the turn. A stream that is never bound simply publishes * nothing - an unattributed reply has no conversation to appear in. + * + * The id is minted HERE, in the one place that already knows a reply has started, rather than being + * passed in by each caller that persists one. A caller that has to remember to thread an id is a + * caller that can forget, and every site that forgot would silently go back to being drawn twice on + * a paired device. */ export function bindChatStream(streamId: string | undefined, conversationId?: string): void { if (!streamId || !conversationId) return - active.set(streamId, { conversationId, content: '', reasoning: '' }) + // A new reply supersedes any identity still unclaimed for this conversation - the previous turn was + // cancelled or failed before it ever became a record, so nothing is going to claim it. + const messageId = crypto.randomUUID() + pendingMessageIds.set(conversationId, messageId) + active.set(streamId, { conversationId, content: '', reasoning: '', messageId }) publish(streamId) } +/** + * Claim the identity minted for this conversation's reply, so its record keeps the id its live + * frames already carried on every paired device. + * + * Take-once: the first record to conclude the turn claims it, and anything written afterwards gets a + * fresh id of its own. Returns undefined when nothing was streamed - a message typed into a + * conversation with no generation behind it is simply new, and mints its own id as it always did. + */ +export function takeChatStreamMessageId(conversationId: string): string | undefined { + const messageId = pendingMessageIds.get(conversationId) + if (messageId !== undefined) pendingMessageIds.delete(conversationId) + return messageId +} + /** Fold one delta into the reply so far and publish the result. */ export function noteChatStreamDelta( streamId: string | undefined, @@ -63,6 +104,7 @@ function publish(streamId: string): void { callHook(HOOKS.syncStreamingState, { conversationId: stream.conversationId, content: stream.content, - reasoning: stream.reasoning + reasoning: stream.reasoning, + messageId: stream.messageId }) } diff --git a/src/main/database.ts b/src/main/database.ts index e54c72b6..e22d5b25 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1256,11 +1256,19 @@ export function addRagMessage( conversationId: string, role: 'user' | 'assistant', content: string, - context?: unknown + context?: unknown, + /** + * The identity this message ALREADY has, when something named it before it was stored. + * + * A streamed reply is named at its first token, so the frames a paired device renders live carry + * the same id as the record that follows. Minting a fresh one here instead is what left the peer + * unable to match the two, so it drew the answer twice until its preview timed out. + */ + knownUuid?: string ): AddedRagMessage { const db = getDB() const contextJson = context ? JSON.stringify(context) : null - const uuid = crypto.randomUUID() + const uuid = knownUuid ?? crypto.randomUUID() // uuid is the cross-device identity for sync (the autoincrement id is device-local). const info = db diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 2360a882..317da64c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -93,7 +93,8 @@ async function regenerateMasterMemory(): Promise { import { bindChatStream, endChatStream, - noteChatStreamDelta + noteChatStreamDelta, + takeChatStreamMessageId } from './chat-stream-state' const streamControllers = new Map() @@ -1180,7 +1181,12 @@ export function setupIPC() { ipcMain.handle( 'rag:add-message', (_, conversationId: string, role: 'user' | 'assistant', content: string, context?: any) => { - return addRagMessage(conversationId, role, content, context) + // A reply that was streamed is already named, and keeps that name: every paired device has been + // rendering it under this id, so the arriving record retires their live preview instead of + // standing beside it. Read from the one owner of "what this device is generating", so no caller + // has to pass it and none can forget to. + const streamed = role === 'assistant' ? takeChatStreamMessageId(conversationId) : undefined + return addRagMessage(conversationId, role, content, context, streamed) } ) From 79bb06ffd8337dc7fa42aadab9c4583bcd62c4a2 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 13 Aug 2026 09:59:16 +0530 Subject: [PATCH 15/43] feat(chat): a file still on its way draws a loader on its own turn The announcement precedes the bytes and carries everything a placeholder needs, so the bubble can name what is coming instead of showing nothing until it lands - a synced image was indistinguishable from one that was never sent, and the only way to learn which was to restart. Held in renderer state and matched on the message UUID, which is the only identity a peer can name: the autoincrement row id is local to one device. Every capture URL in this screen now goes through the one writer, so the Windows dialect is not re-derived per call site. --- src/preload/index.ts | 25 +++++++++ src/renderer/src/components/MemoryChat.tsx | 61 ++++++++++++++++++---- src/renderer/src/env.d.ts | 17 ++++++ 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/preload/index.ts b/src/preload/index.ts index 1eae4bad..ecb2df38 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -155,6 +155,31 @@ const offGridApi = { ipcRenderer.on('rag:conversations-changed', subscription) return unsubscribe('rag:conversations-changed', subscription) }, + /** + * The files a chat is still waiting for, whenever that set changes. + * + * Announced rather than fetched: the announcement arrives before the bytes, so the only device that + * knows a file is on its way is this one, and it knows the moment the control lands. There is no + * matching `get`, deliberately - the main process sends the whole set on every change, so a late + * subscriber gets the truth on the next arrival rather than reading a snapshot that is already old. + */ + onIncomingSharedFiles: ( + callback: ( + files: { + syncId: string + name: string + fileSize: number + mimeType: string + kind: string + conversationId?: string + messageId?: string + }[] + ) => void + ) => { + const subscription = (_event: unknown, files: never[]): void => callback(files) + ipcRenderer.on('pro:sync:incoming-files', subscription) + return unsubscribe('pro:sync:incoming-files', subscription) + }, searchRagConversationIds: (query: string) => ipcRenderer.invoke('rag:search-conversation-ids', query), setRagConversationProject: (id: string, projectId: string | null) => diff --git a/src/renderer/src/components/MemoryChat.tsx b/src/renderer/src/components/MemoryChat.tsx index 53194642..39b372ea 100644 --- a/src/renderer/src/components/MemoryChat.tsx +++ b/src/renderer/src/components/MemoryChat.tsx @@ -64,6 +64,7 @@ import { CollapsibleContent, CollapsibleTrigger } from '@renderer/components/ui/collapsible' +import { captureUrlForPath } from '../../../shared/ogcapture-url' import { Plus, Paperclip, @@ -354,7 +355,7 @@ function mapRagMessages(raw: any[]): ChatMessage[] { provenance: turn.provenance, // Read through the one reader, so a row that names its image on the mesh and a row that // only remembers a path both render, and neither is decoded here. - image: imageReference ? `ogcapture://${imageReference.path}` : undefined, + image: imageReference ? captureUrlForPath(imageReference.path) : undefined, imagePath: imageReference?.path, imageMetadata: ctx?.imageMetadata, // Attachments persisted on the user turn (clickable chips survive reload). @@ -542,6 +543,25 @@ export function MemoryChat({ // re-check periodically (the user can switch models from the Models screen). const [chatVision, setChatVision] = useState(true) const [attachWarn, setAttachWarn] = useState(null) + /** + * Files a peer has announced for this chat whose bytes have not arrived. + * + * Held here and never in the message: the wait is true of THIS device only, and a message is synced, + * so writing it onto the turn would tell peers that already hold the file to wait for it. The main + * process sends the whole set on every change, so this replaces rather than merges. + */ + const [incomingFiles, setIncomingFiles] = useState([]) + useEffect(() => { + const off = window.api.onIncomingSharedFiles?.((files) => setIncomingFiles(files)) + return () => off?.() + }, []) + // Matched on the message's UUID, which is what `id` carries here (`String(m.uuid ?? m.id)`) and is + // the only identity a peer can name β€” the autoincrement row id is local to one device. + const incomingFilesFor = useCallback( + (messageUuid: string | undefined): IncomingSharedFile[] => + messageUuid ? incomingFiles.filter((file) => file.messageId === messageUuid) : [], + [incomingFiles] + ) useEffect(() => { const check = (): void => { void (window.api as { chatVisionAvailable?: () => Promise }) @@ -3023,7 +3043,7 @@ export function MemoryChat({ > {thumb ? ( {s.name} @@ -3292,6 +3312,25 @@ export function MemoryChat({
) : null} + {/* Announced by a peer, bytes still coming. Drawn from the announcement, so + the row names the real file instead of leaving the turn looking empty + until it lands β€” which is what made a synced image look like a lost one. */} + {incomingFilesFor(message.id).map((incoming) => ( +
+ + + + + + + {incoming.name} + +
+ ))} {message.attachments && message.attachments.length > 0 ? (
{message.attachments.map((att, i) => { @@ -3305,7 +3344,7 @@ export function MemoryChat({ if (att.kind === 'image' && att.path) { closePanels() setLightbox({ - url: `ogcapture://${att.path}`, + url: captureUrlForPath(att.path), path: att.path }) } else if (att.text || att.path) { @@ -3905,7 +3944,7 @@ export function MemoryChat({ {/* Screen captures: show the actual frame, click β†’ Replay seeked to that moment */} {u.kind === 'screen' && u.imagePath ? ( @@ -4480,7 +4519,7 @@ export function MemoryChat({
) : ( -
- - - - - - - {waitingLabel({ noMemory, hasProject: !!activeProjectId })} - -
+ )} ) : null} diff --git a/src/renderer/src/components/__tests__/MemoryChat.chat-lifecycle.test.tsx b/src/renderer/src/components/__tests__/MemoryChat.chat-lifecycle.test.tsx index c3089123..9630b207 100644 --- a/src/renderer/src/components/__tests__/MemoryChat.chat-lifecycle.test.tsx +++ b/src/renderer/src/components/__tests__/MemoryChat.chat-lifecycle.test.tsx @@ -56,7 +56,11 @@ describe(' - chat lifecycle integration (#36-#42, #47-#48)', () => boundary.resolve(0, 'Choose plan B because it is reversible.') - expect(await screen.findByRole('button', { name: /thought process/i })).toBeTruthy() + const thoughtProcess = await screen.findByRole('button', { name: /thought process/i }) + const thoughtProcessLabel = screen.getByText('Thought process') + expect(thoughtProcessLabel.classList.contains('whitespace-nowrap')).toBe(true) + await user.click(thoughtProcess) + expect(await screen.findByText('First compare risk, then reversibility.')).toBeTruthy() expect(screen.getByText('Choose plan B because it is reversible.')).toBeTruthy() expect(screen.queryByText(/<\/?think>/i)).toBeNull() }) From 8e057972efddbcc0eb0a6d41b4019958a1dfac10 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 13 Aug 2026 14:29:34 +0530 Subject: [PATCH 17/43] fix(chat): preview synced image attachments --- src/renderer/src/components/MemoryChat.tsx | 83 ++++++++++++------- .../__tests__/MemoryChat.image.test.tsx | 40 +++++++++ 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/renderer/src/components/MemoryChat.tsx b/src/renderer/src/components/MemoryChat.tsx index 8413cd27..b6c89ae8 100644 --- a/src/renderer/src/components/MemoryChat.tsx +++ b/src/renderer/src/components/MemoryChat.tsx @@ -382,6 +382,34 @@ function ImageMetadata({ ) } +function ChatImagePreview({ + src, + path, + alt = 'Generated', + metadata, + className, + onOpen +}: { + src: string + path?: string + alt?: string + metadata?: ImageGenerationMetadata + className: string + onOpen: (image: { url: string; path?: string }) => void +}): React.JSX.Element { + return ( +
+ {alt} onOpen({ url: src, path })} + className={className} + /> + +
+ ) +} + // Core (free) suggestions β€” generic chat/build/image. Pro adds memory-aware ones. const ASK_EXAMPLES = [ 'Explain how RAG works, simply', @@ -3128,20 +3156,13 @@ export function MemoryChat({ // Generated image in voice mode: show the image, no audio bubble. if (message.image) { return ( -
- Generated - setLightbox({ - url: message.image as string, - path: message.imagePath - }) - } - className="max-w-[20rem] cursor-zoom-in rounded-md border border-neutral-800 transition-opacity hover:opacity-90" - /> - -
+ ) } const transcript = messageToSpeakable( @@ -3288,6 +3309,19 @@ export function MemoryChat({ {message.attachments && message.attachments.length > 0 ? (
{message.attachments.map((att, i) => { + if (att.kind === 'image' && att.path) { + const src = captureUrlForPath(att.path) + return ( + + ) + } const viewable = !!att.text || (att.kind === 'image' && !!att.path) return (
+ ) : null} diff --git a/src/renderer/src/components/__tests__/MemoryChat.image.test.tsx b/src/renderer/src/components/__tests__/MemoryChat.image.test.tsx index 02dafa23..b397a121 100644 --- a/src/renderer/src/components/__tests__/MemoryChat.image.test.tsx +++ b/src/renderer/src/components/__tests__/MemoryChat.image.test.tsx @@ -612,6 +612,46 @@ describe(' image and vision release journeys', () => { }) }) + it('renders a synced image attachment inline and opens the existing lightbox', async () => { + const conv = conversation('c-synced-image', 'Synced image') + installApi({ + active: FULL, + models: [FULL], + conversations: [conv], + messages: { + [conv.id]: [ + { + uuid: 'message-image-1', + role: 'assistant', + content: 'Generated image for: "Draw a dog"', + context: JSON.stringify({ + attachments: [ + { + id: 'image-1', + name: 'dog.png', + kind: 'image', + path: '/received/dog.png' + } + ] + }) + } + ] + } + }) + const user = userEvent.setup() + renderChat({ conversationId: conv.id }) + + const image = await screen.findByAltText('dog.png') + expect(image.getAttribute('src')).toBe('ogcapture:///received/dog.png') + expect(screen.queryByText('image')).toBeNull() + + await user.click(image) + expect(screen.getByRole('dialog', { name: 'Generated image preview' })).toBeTruthy() + expect(screen.getByAltText('Generated preview').getAttribute('src')).toBe( + 'ogcapture:///received/dog.png' + ) + }) + it('shows live progress, renders one generated image, and opens and saves it (#61, #67)', async () => { const turn = deferred() const boundary = installApi({ From d37f0f33082e6e5f5929706efb01e075907954cf Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Fri, 14 Aug 2026 07:12:17 +0530 Subject: [PATCH 18/43] fix(chat): present synced generation results --- src/renderer/src/components/ChatToolRows.tsx | 64 ++++ src/renderer/src/components/MemoryChat.tsx | 345 +++++++++++------- .../MemoryChat.chat-lifecycle.test.tsx | 30 ++ .../__tests__/MemoryChat.image.test.tsx | 138 ++++++- ...MemoryChat.tool-calls.integration.test.tsx | 98 ++++- .../__tests__/harness/chat-boundary.tsx | 20 +- .../src/lib/__tests__/stream-reducer.test.ts | 31 +- src/renderer/src/lib/stream-reducer.ts | 57 ++- 8 files changed, 621 insertions(+), 162 deletions(-) create mode 100644 src/renderer/src/components/ChatToolRows.tsx diff --git a/src/renderer/src/components/ChatToolRows.tsx b/src/renderer/src/components/ChatToolRows.tsx new file mode 100644 index 00000000..4918e871 --- /dev/null +++ b/src/renderer/src/components/ChatToolRows.tsx @@ -0,0 +1,64 @@ +import type { ChatStreamTool, ProjectedSyncedTool } from '@offgrid/sync' +import { CaretDown, Wrench } from '@phosphor-icons/react' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@renderer/components/ui/collapsible' + +type DisplayTool = + | Pick + | ChatStreamTool + +interface ChatToolRowsProps { + tools: readonly DisplayTool[] | undefined +} + +/** One tool layout for live previews and durable assistant messages. */ +export function ChatToolRows({ tools }: Readonly): React.JSX.Element | null { + const visible = (tools ?? []).filter((tool) => tool.name !== 'search_memory') + if (visible.length === 0) return null + + return ( +
+ {visible.map((tool, index) => { + const running = tool.status === 'running' + const result = tool.result ?? '' + const hasDetails = !running && result.trim().length > 0 + const durationMs = 'durationMs' in tool ? tool.durationMs : undefined + const completionLabel = `${tool.status === 'failed' ? 'Failed' : 'Completed'}${ + durationMs !== undefined ? ` in ${Math.round(durationMs)} ms` : '' + }` + return ( + + + + {hasDetails ? ( + + {result} + + ) : null} + + ) + })} +
+ ) +} diff --git a/src/renderer/src/components/MemoryChat.tsx b/src/renderer/src/components/MemoryChat.tsx index b6c89ae8..e6eb49be 100644 --- a/src/renderer/src/components/MemoryChat.tsx +++ b/src/renderer/src/components/MemoryChat.tsx @@ -12,6 +12,7 @@ import { useActiveModelSummary } from '@renderer/hooks/useActiveModelSummary' import { shouldFollowBottom } from '@renderer/lib/scroll-follow' import { chatListPreviewLine, + isSupportingChatContext, projectSyncedMessageTurn, type ProjectedSyncedTool, type RecordProvenance, @@ -24,6 +25,7 @@ import remarkBreaks from 'remark-breaks' import { getSlot, SLOTS } from '@/bootstrap/slotRegistry' import { ChatLoadingCard } from './ChatLoadingCard' import { ChatThinkingBlock } from './ChatThinkingBlock' +import { ChatToolRows } from './ChatToolRows' import { ArtifactCanvas, parseArtifact, type Artifact } from './ArtifactCanvas' import { VoiceBubble, stopAllVoicePlayback } from './VoiceBubble' import { SkillsPanel } from './SkillsPanel' @@ -182,6 +184,25 @@ function noticeText(content: string): string { return content.replace(/^_([\s\S]*)_$/, '$1').trim() } +/** + * Mobile persists this short-lived row while it rewrites an image prompt, then updates the SAME + * message to a labelled reasoning block. It is lifecycle state, not an assistant answer: drawing + * reply actions on it made Speak / Copy / Regenerate target text that was about to be replaced. + */ +function isPromptEnhancementContent(content: unknown): content is string { + return typeof content === 'string' && /^Enhancing your prompt(?:\.{3}|…)$/.test(content.trim()) +} + +function isPromptEnhancementStatus(message: ChatMessage): boolean { + return ( + message.role === 'assistant' && + !message.image && + !message.reasoning?.trim() && + !message.toolCalls?.length && + isPromptEnhancementContent(message.content) + ) +} + type AskBlock = { question: string; options: string[]; multiSelect: boolean } // Detect a model-emitted interactive question: ```ask { question, options, multiSelect }``` @@ -298,7 +319,7 @@ interface MemoryChatProps { } function mapRagMessages(raw: any[]): ChatMessage[] { - return raw.flatMap((m: any) => { + return raw.flatMap((m: any) => { let ctx: RagContext | undefined if (m.context && typeof m.context === 'string') { try { @@ -316,6 +337,13 @@ function mapRagMessages(raw: any[]): ChatMessage[] { originDeviceName: m.origin_device_name } : undefined + // Shared correctly excludes this temporary row from the portable ANSWER projection. Desktop + // still needs the local database row as lifecycle UI until the same UUID is rewritten as the + // durable Enhanced prompt disclosure. Admit only this exact producer-owned sentence here. + if (m.role === 'assistant' && isPromptEnhancementContent(m.content)) { + const id = String(m.uuid ?? m.id ?? '') + return id ? [{ id, role: 'assistant', content: m.content, provenance }] : [] + } const turn = projectSyncedMessageTurn({ id: String(m.uuid ?? m.id), role: m.role, @@ -399,12 +427,7 @@ function ChatImagePreview({ }): React.JSX.Element { return (
- {alt} onOpen({ url: src, path })} - className={className} - /> + {alt} onOpen({ url: src, path })} className={className} />
) @@ -567,6 +590,28 @@ export function MemoryChat({ }, [] ) + // A peer can update one durable message twice in quick succession (the prompt-enhancement + // placeholder, then its final disclosure). SQLite reads started for both broadcasts may finish + // out of order; only the newest read is allowed to replace the rendered conversation. + const conversationMessageLoadVersionRef = useRef>(new Map()) + const loadLatestConversationMessages = useCallback( + async (conversationId: string): Promise => { + const nextVersion = (conversationMessageLoadVersionRef.current.get(conversationId) ?? 0) + 1 + conversationMessageLoadVersionRef.current.set(conversationId, nextVersion) + const rawMessages = await window.api.getRagMessages(conversationId) + if (conversationMessageLoadVersionRef.current.get(conversationId) !== nextVersion) return null + return mapRagMessages(rawMessages) + }, + [] + ) + const refreshConversationMessages = useCallback( + async (conversationId: string): Promise => { + const nextMessages = await loadLatestConversationMessages(conversationId) + if (!nextMessages) return + setConvMessages(conversationId, nextMessages) + }, + [loadLatestConversationMessages, setConvMessages] + ) const [input, setInput] = useState('') const [attachments, setAttachments] = useState([]) // Whether the active chat model can read images. Gate image attachment on this and @@ -638,6 +683,13 @@ export function MemoryChat({ // Active tab's messages (derived) + a shim so the existing active-conversation call // sites keep working. The send path targets its own conv via setConvMessages instead. const messages = messagesByConv[activeConversationId ?? NEW_CHAT] ?? EMPTY_MSGS + const promptEnhancementActive = messages.some(isPromptEnhancementStatus) + const promptEnhancementComplete = messages.some( + (message) => + message.role === 'assistant' && + message.reasoningLabel?.trim().toLowerCase() === 'enhanced prompt' && + !!message.reasoning?.trim() + ) const setMessages = useCallback( (updater: ChatMessage[] | ((prev: ChatMessage[]) => ChatMessage[])): void => { setConvMessages(activeConversationId, updater) @@ -936,7 +988,28 @@ export function MemoryChat({ }, []) const markdownComponents: Components = { - p: ({ children }) =>

{children}

, + h1: ({ children }) => ( +

{children}

+ ), + h2: ({ children }) => ( +

{children}

+ ), + h3: ({ children }) => ( +

{children}

+ ), + p: ({ children }) =>

{children}

, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) =>
  • {children}
  • , + blockquote: ({ children }) => ( +
    + {children} +
    + ), + strong: ({ children }) => {children}, + hr: () =>
    , a: ({ href, children }) => ( {children} @@ -952,7 +1025,7 @@ export function MemoryChat({ ) }, - pre: ({ children }) =>
    {children}
    + pre: ({ children }) =>
    {children}
    } // Citation-aware markdown components: `[S2]` in an answer is rewritten to a @@ -1057,7 +1130,8 @@ export function MemoryChat({ setActiveProjectId((first as { project_id?: string | null }).project_id ?? null) setOpenTabs([first.id]) try { - setConvMessages(first.id, mapRagMessages(await window.api.getRagMessages(first.id))) + const nextMessages = await loadLatestConversationMessages(first.id) + if (nextMessages) setConvMessages(first.id, nextMessages) } catch { setConvMessages(first.id, []) } @@ -1234,14 +1308,6 @@ export function MemoryChat({ return () => off?.() }, []) - const refreshConversationMessages = useCallback(async (conversationId: string): Promise => { - const rawMessages = await window.api.getRagMessages(conversationId) - setMessagesByConv((prev) => ({ - ...prev, - [conversationId]: mapRagMessages(rawMessages) - })) - }, []) - // Image jobs survive this component. Subscribe before reading the snapshot so a // navigation/remount cannot miss the transition between status and observation. // Cleanup only detaches observers; explicit Stop is the sole cancellation path. @@ -1296,19 +1362,18 @@ export function MemoryChat({ setActiveConversationId(convId) setActiveProjectId(conversations.find((c) => c.id === convId)?.project_id ?? null) try { - const rawMessages = await window.api.getRagMessages(convId) + const nextMessages = await loadLatestConversationMessages(convId) + if (!nextMessages) return // Refresh from DB, but never clobber an in-flight stream for this conversation. setMessagesByConv((prev) => - prev[convId]?.some((m) => m.streaming) - ? prev - : { ...prev, [convId]: mapRagMessages(rawMessages) } + prev[convId]?.some((m) => m.streaming) ? prev : { ...prev, [convId]: nextMessages } ) } catch (e) { console.error('Failed to load messages:', e) setMessagesByConv((prev) => (prev[convId] ? prev : { ...prev, [convId]: [] })) } }, - [activeConversationId, conversations] + [activeConversationId, conversations, loadLatestConversationMessages] ) // Close a chat tab; fall back to another open tab (or a fresh chat) if it was active. @@ -1345,10 +1410,7 @@ export function MemoryChat({ conversationId === activeConversationId && !generatingConvs.has(conversationId) ) { - setConvMessages( - conversationId, - mapRagMessages(await window.api.getRagMessages(conversationId)) - ) + await refreshConversationMessages(conversationId) } await loadConversations() } catch (error) { @@ -1358,7 +1420,7 @@ export function MemoryChat({ }) return () => off?.() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeConversationId, generatingConvs]) + }, [activeConversationId, generatingConvs, refreshConversationMessages]) // Open a target passed from the Projects tab (an existing chat, or a new chat // scoped to a project). Resolves project from the DB to avoid stale state. @@ -1372,7 +1434,8 @@ export function MemoryChat({ setOpenTabs((t) => (t.includes(convId) ? t : [...t, convId])) const conv = await window.api.getRagConversation(convId) setActiveProjectId((conv as { project_id?: string | null }).project_id ?? null) - setConvMessages(convId, mapRagMessages(await window.api.getRagMessages(convId))) + const nextMessages = await loadLatestConversationMessages(convId) + if (nextMessages) setConvMessages(convId, nextMessages) } else if (openTarget.projectId) { setActiveConversationId(null) setConvMessages(null, []) @@ -1761,7 +1824,8 @@ export function MemoryChat({ }) const toolCalls = (tr?.toolCalls || []).map((c: { name: string; result: string }) => ({ name: c.name, - result: c.result + result: c.result, + status: 'completed' as const })) const context = tr?.unified?.length ? { unified: tr.unified } : undefined // Persist the citation sources + tool calls so they survive a reload. @@ -1797,11 +1861,18 @@ export function MemoryChat({ ) const toolCtxWithReasoning = buildAssistantContext(toolCtx, { reasoning: toolReasoning }) if (voiceMode) setAutoPlayId(toolStreamId) - // Deferred image generation: the tool loop only RECORDS the prompt (it never - // generates inline, which would evict the LLM). Generate + attach here AFTER - // the text turn - same path as the ```image fence block below. + // Deferred image generation: the tool loop only RECORDS prompts (it never generates inline, + // which would evict the LLM). Each completed request gets one generated file and one durable + // assistant image message. A message context has one imageRef by design; putting two results + // on one row would make the last context write replace the first association. + const imageRequests = + tr?.imageRequests?.length > 0 + ? tr.imageRequests + : tr?.imageRequest?.prompt + ? [tr.imageRequest] + : [] if ( - tr?.imageRequest?.prompt && + imageRequests.length > 0 && window.api.generateImage && !cancelledRef.current.has(convId) ) { @@ -1812,40 +1883,53 @@ export function MemoryChat({ setImgProgress(null) setImageGenConv(convId) try { - const img = await window.api.generateImage({ - prompt: tr.imageRequest.prompt, - conversationId: convId, - projectId: projectId - }) - setConvMessages(convId, (prev) => - prev.map((m) => - m.id === toolStreamId ? { ...m, image: img.dataUrl, imagePath: img.path } : m - ) - ) - try { - const stored = await window.api.addRagMessage( - convId, - 'assistant', - answer, - withGeneratedImageReference(toolCtxWithReasoning ?? {}, { - id: img.syncId, - path: img.path + await window.api.addRagMessage(convId, 'assistant', answer, toolCtxWithReasoning) + } catch { + /* The answer remains on screen; image rows can still be persisted independently. */ + } + try { + for (const imageRequest of imageRequests) { + if (cancelledRef.current.has(convId)) break + setImgProgress(null) + try { + const img = await window.api.generateImage({ + prompt: imageRequest.prompt, + conversationId: convId, + projectId: projectId }) - ) - await announceImageMessagePersisted(convId, stored.uuid) - } catch { - /* ignore */ - } - } catch (e) { - // Persist the TEXT answer regardless of why the image failed β€” including - // a cancel. The text turn already finished and is on screen; a cancelled - // (or failed) IMAGE must not drop it, or it vanishes on reload (D12). Only - // the image is lost. - void e - try { - await window.api.addRagMessage(convId, 'assistant', answer, toolCtxWithReasoning) - } catch { - /* ignore */ + const imageContent = `Generated for: ${imageRequest.prompt}` + let imageMessageId: string = crypto.randomUUID() + try { + const stored = await window.api.addRagMessage( + convId, + 'assistant', + imageContent, + withGeneratedImageReference(undefined, { + id: img.syncId, + path: img.path + }) + ) + imageMessageId = stored.uuid + await announceImageMessagePersisted(convId, stored.uuid) + } catch { + /* Keep the generated file visible even if this database write fails. */ + } + setConvMessages(convId, (prev) => [ + ...prev, + { + id: imageMessageId, + role: 'assistant', + content: imageContent, + image: img.dataUrl, + imagePath: img.path + } + ]) + } catch (error) { + // One failed image does not erase or block another completed tool request. Stop is + // the exception: it cancels the active runtime and ends the remaining local work. + if (cancelledRef.current.has(convId)) break + console.error('Deferred tool image generation failed', error) + } } } finally { setImgProgress(null) @@ -3134,7 +3218,34 @@ export function MemoryChat({ {noticeText(message.content)} - ) : voiceMode && message.role !== 'tool' ? ( + ) : isPromptEnhancementStatus(message) ? ( +
    + +
    + ) : message.role === 'tool' ? ( +
    + +
    + ) : voiceMode ? (
    ) } + if ( + isSupportingChatContext({ + answer: message.content, + reasoning: message.reasoning, + reasoningLabel: message.reasoningLabel + }) + ) { + return ( + + ) + } // Generated image in voice mode: show the image, no audio bubble. if (message.image) { return ( @@ -3218,36 +3343,6 @@ export function MemoryChat({ ) : null}
    ) : null} - {/* Tool calls as their own entry, between thinking and the answer bubble. - search_memory is shown as interactive Source cards below, so skip its chip. */} - {(() => { - const chips = (message.toolCalls || []).filter( - (tc) => tc.name !== 'search_memory' - ) - return chips.length > 0 ? ( -
    - {chips.map((tc, i) => ( - - ))} -
    - ) : null - })()}
    - {message.role === 'tool' ? ( -
    - - - {message.toolName || 'Tool result'} - - - {message.turnStatus === 'failed' ? 'Failed' : 'Completed'} - {message.generationTimeMs !== undefined - ? ` in ${Math.round(message.generationTimeMs)} ms` - : ''} - -
    - ) : null} {/* Announced by a peer, bytes still coming. Drawn from the announcement, so the row names the real file instead of leaving the turn looking empty until it lands β€” which is what made a synced image look like a lost one. */} @@ -3368,6 +3451,15 @@ export function MemoryChat({ })}
    ) : null} + {message.image ? ( + + ) : null} {editingId === message.id ? (