From 1f55a085d4b602cbb16406eac390b737deac6488 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:33:58 +0200 Subject: [PATCH 1/6] docs: sync TODO/AUDIT with the 2026-07-28 local-AI fixes + Dependabot batch Light-touch update reflecting today's session: the two remaining #266 fixes (desktop discovery build-config bug, browser status badge reconciliation), the 13-of-18 Dependabot merge outcome with the 5 documented real blockers left open, and the Issue #60 status comment. Co-Authored-By: Claude Sonnet 5 --- AUDIT.md | 9 +++++++++ TODO.md | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/AUDIT.md b/AUDIT.md index 34f9585f..dffa363d 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -12,6 +12,15 @@ **Quality gate (2026-06-21 — feature-flag catalog + grouped Settings + ProForge opt-in):** lint ✅ · typecheck ✅ (tsgo) · i18n:check ✅ (**2793 keys × 17 locales**) · parity:check ✅ (0 drifts) · suppressions ratchet ✅ (52, no new) · targeted unit tests ✅ (127: slice 89, catalog 7, flagDependencies 5, FeatureFlagsSection 15, FeatureFlagsAndOverview 11). `enableProForge` default flipped to opt-in (now **17 on / 6 off**); `featureCatalog.ts` reconciled to all 23 flags with `defaultOn` **derived** from the slice (drift now structurally impossible — guarded by `tests/unit/featureCatalog.test.ts`). Shipped as a standalone PR off `main`. +## v1.24.1 Local-AI reliability + Dependabot batch (2026-07-28) + +**Quality gate (2026-07-28):** lint ✅ · typecheck ✅ · i18n:check ✅ (**2834 keys × 19 locales** — `ru`/`ko` added since the v1.24.0 count above; `settings.ai.providerStatusUnavailableBrowser` new key) · targeted unit tests ✅ (PR #272: 40 · PR #273: 64, incl. new badge-reconciliation + scan-endpoint-order coverage). + +- **#266 (Ollama/LM Studio/vLLM), two remaining halves fixed**, both root-caused with a real build+runtime repro rather than guessed: (1) desktop discovery was still broken after #269 because `vite.config.ts` externalized `@tauri-apps/*` even for the Tauri build itself, leaving `services/localServerHttp.ts`'s plugin-http import unresolvable in the packaged app — fixed via a shared `isTauriBuild()` check (PR #272); (2) the browser status badge and the desktop-only banner were driven by two unreconciled state signals, so the badge always read "Ready" next to a banner saying otherwise — fixed with a distinct "Not available in browser" label (PR #273). +- **Dependabot backlog triaged:** 13 of 18 open PRs merged directly; the remaining 5 (AI-SDK v4 family, biome 2.5.x, dev-tooling/Babel 8) each had a real breaking-change blocker, root-caused and fixed with real code (AI-SDK family combined into PR #275; biome and dev-tooling fixes pushed directly onto their Dependabot branches) rather than force-merged or suppressed. See `TODO.md` § v1.24.1 for the full breakdown. +- **Issue #60 (vendor-fork audit):** confirmed done (fork `10.3.0-sc2`, `verify:vendor` CI guard); status comment posted, issue stays open per its own standing-reminder policy. +- **CLAUDE.md doc-drift correction:** locale roster count (17→19) and per-locale module count (20→21) were stale relative to `i18n/locales.ts` (the actual SSOT). + ## v1.24.0 Dependency Hygiene + Onboarding + Docs Truth-up (2026-06-21, PR F) - **`joi` override (accepted risk, KEPT):** `pnpm-workspace.yaml` `overrides.joi: ^18.2.1` pins the patched `joi` pulled transitively via `wait-on` (Storybook/test-runner wait helper), mitigating **GHSA-q7cg-457f-vx79** (unpublished jsdom exposure in `@hapi/statehood`). Still required — `wait-on@9.x` still depends on `joi`. `pnpm audit --audit-level=high` clean with the override in place; rationale documented inline in `pnpm-workspace.yaml` and here. diff --git a/TODO.md b/TODO.md index edfa7b49..f73c6794 100644 --- a/TODO.md +++ b/TODO.md @@ -47,6 +47,17 @@ Status: 🔄 in progress | ⬜ open | ✅ done --- +## v1.24.1 — Local-AI reliability + Dependabot batch (2026-07-28) + +- ✅ **Desktop Ollama/LM Studio/vLLM discovery fixed (#266)** — `vite.config.ts`'s `rollupOptions.external` was unconditionally externalizing `@tauri-apps/*` even for the Tauri desktop build itself, leaving `services/localServerHttp.ts`'s `@tauri-apps/plugin-http` import unresolvable in the packaged `.deb`/`.msi`. Fixed via a shared `isTauriBuild()` check; see the 2026-07-28 update in [ADR 0012](docs/adr/0012-local-server-connectivity-tauri-http.md). +- ✅ **Misleading "Ready" status badge for Ollama in the browser fixed (#266)** — the status badge and the desktop-only banner were driven by two unreconciled state signals; badge now shows a distinct "Not available in browser" label. Scan also now prefers native `/api/tags` over the OpenAI-compat shim for Ollama. +- ✅ **Dependabot batch** — 13 of 18 open PRs merged directly (Actions bumps, small JS/dev-tooling patches, Tauri/Rust crates, the tauri-deps group). The remaining 5 all had real breaking-change blockers, root-caused and fixed rather than force-merged or suppressed: + - **#255 / #249 / #253 / #250** (`@ai-sdk/google`/`@ai-sdk/openai`/`@ai-sdk/react`/`ai` major bumps) — `@ai-sdk/google@4` alone emits `LanguageModelV4`, incompatible with the pre-upgrade `ai` package's `LanguageModel` type; root cause was a partial-family upgrade. Fixed by bumping all four together in **PR #275** (`feat/ai-sdk-v4-upgrade`), which supersedes these 4 PRs — closed once #275 merges. + - **#252** (`@biomejs/biome` 2.4→2.5) — schema-version mismatch plus 2 real new lint findings (`noUnsafeOptionalChaining` ×4, `noUndeclaredEnvVars` ×21 via `turbo.json` `globalEnv`) fixed with real code changes, no suppressions; pushed directly onto the Dependabot branch. + - **#265** (dev-tooling group) — `@babel/core@8` transitively broke `react-docgen@8.0.3`'s Storybook build (`loadPartialConfig` removed); fixed by capping the existing `pnpm-workspace.yaml` override at `<8`, verified with a real `build-storybook` run; pushed directly onto the Dependabot branch. +- ✅ **Issue #60 (vendor-fork audit)** — status comment posted confirming the audit is done (fork at `10.3.0-sc2`, `verify:vendor` CI guard); issue stays open per its own "permanent reminder" note. +- ✅ **CLAUDE.md locale-count drift fixed** — i18n section undercounted the locale roster (17 vs. the actual 19 — `ru`/`ko` were added in a prior PR but never reflected) and the per-locale module count (20 vs. 21). + ## Dependency-Hygiene Backlog (carried forward) > `.npmrc` Hardening (`strict-dep-builds=true`, `block-exotic-subdeps=true`, `minimum-release-age=10080`) ist bereits aktiv. From 434ac15083003c2eda1f2192718ef07fbdf16e0c Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:53:28 +0200 Subject: [PATCH 2/6] security(ci): drop persisted checkout credentials across 7 workflows (CWE-522) None of the affected jobs push or commit after cloning, so persisting the GITHUB_TOKEN via actions/checkout was unnecessary attack surface flagged by zizmor/CodeRabbit. Sets persist-credentials: false on all 14 checkout steps in ci.yml, codeql.yml, docker.yml, mutation.yml, storybook-debug.yml, tauri-build.yml, and voice-nightly.yml. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 15 +++++++++++++++ .github/workflows/codeql.yml | 2 ++ .github/workflows/docker.yml | 2 ++ .github/workflows/mutation.yml | 2 ++ .github/workflows/storybook-debug.yml | 2 ++ .github/workflows/tauri-build.yml | 2 ++ .github/workflows/voice-nightly.yml | 2 ++ CHANGELOG.md | 6 ++++++ 8 files changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b0879e6..045fbf56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: fetch-depth: 0 + persist-credentials: false - uses: ./.github/actions/setup # QNBS-v3: pnpm's audit client cannot decode force-gzipped responses from some npm CDN edge @@ -99,6 +100,8 @@ jobs: node-version: ['22', '24'] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup with: node-version: ${{ matrix.node-version }} @@ -180,6 +183,8 @@ jobs: id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Build application @@ -268,6 +273,8 @@ jobs: needs: [quality] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup # QNBS-v3: cache Playwright browsers by version so re-runs don't re-download ~400 MB each time. @@ -321,6 +328,8 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Cache Playwright browsers @@ -358,6 +367,8 @@ jobs: continue-on-error: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Download build artifact @@ -403,6 +414,8 @@ jobs: needs: [quality] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup # QNBS-v3: Cache Playwright browsers between runs — saves ~60 s install time. @@ -460,6 +473,8 @@ jobs: needs: [build] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Download dist artifact diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 77f054d5..dea805f6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,6 +28,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d92172f4..d58fb44d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -24,6 +24,8 @@ jobs: steps: # QNBS-v3: SHA-pinned to prevent supply-chain attacks via mutable tags - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - name: Set up QEMU uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 7407a9b9..5924ca59 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -64,6 +64,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup diff --git a/.github/workflows/storybook-debug.yml b/.github/workflows/storybook-debug.yml index 1afd2fc2..cfbfb832 100644 --- a/.github/workflows/storybook-debug.yml +++ b/.github/workflows/storybook-debug.yml @@ -32,6 +32,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Build Storybook diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 4911f476..f7bbd156 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -35,6 +35,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false # QNBS-v3: composite action handles pnpm/node setup + frozen install. - uses: ./.github/actions/setup diff --git a/.github/workflows/voice-nightly.yml b/.github/workflows/voice-nightly.yml index e82264df..d801b097 100644 --- a/.github/workflows/voice-nightly.yml +++ b/.github/workflows/voice-nightly.yml @@ -26,6 +26,8 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false - uses: ./.github/actions/setup - name: Cache Playwright browsers diff --git a/CHANGELOG.md b/CHANGELOG.md index eaabfe3d..140e60e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -171,6 +171,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- **GitHub Actions workflows drop persisted checkout credentials (CWE-522, zizmor/CodeRabbit).** + `actions/checkout` leaves the `GITHUB_TOKEN` in the local git config by default so later steps can + push; none of the jobs across `ci.yml` (8 checkout steps), `codeql.yml`, `docker.yml`, + `mutation.yml`, `storybook-debug.yml`, `tauri-build.yml`, or `voice-nightly.yml` push or commit + after cloning, so the token had no reason to persist beyond the checkout step. All 14 checkout + steps now set `persist-credentials: false`. - **`@babel/core` security override bounded to the 7.x line, fixing a broken Storybook build.** The `pnpm-workspace.yaml` override for GHSA-4x5r-pxfx-6jf8 (`>=7.29.6`) had no upper bound, unlike every sibling override in the same block. When Babel 8.0.1 was published, pnpm resolved it for From ac48b4d331a6a058b60e81ef4758c806dfdc5b93 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:23:21 +0200 Subject: [PATCH 3/6] fix(build): invalid ltr/rtl Tailwind variant + 3 ineffective dynamic imports ToggleSwitch used a malformed [dir:ltr]/[dir:rtl] arbitrary variant that compiled to the invalid CSS pseudo-class :is(dir:ltr), producing a lightningcss warning on every production build. Replaced with Tailwind's built-in ltr:/rtl: direction variants, which correctly target the dir attribute App.tsx already sets on . Also converts 3 dynamic import() calls (transientUiStore, ecoModeService, spotlightTour) to static imports: Rolldown flagged all three as unable to move their module into a separate chunk since each is also statically imported elsewhere in the eager bundle, making the dynamic import dead weight with no code-splitting benefit. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ app/listenerMiddleware.ts | 8 ++++---- components/settings/SettingsShared.tsx | 2 +- services/commands/commandDefinitions.tsx | 5 ++--- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 140e60e1..4d721bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`pnpm run build` warnings: invalid `[dir:ltr]`/`[dir:rtl]` Tailwind arbitrary variants and 3 + ineffective dynamic imports.** `ToggleSwitch`'s thumb translate used a malformed custom variant + (`[dir:ltr]:translate-x-5 [dir:rtl]:-translate-x-5`) that Tailwind compiled to the invalid CSS + pseudo-class `:is(dir:ltr)` (lightningcss minify warning on every production build) — replaced + with Tailwind's built-in `ltr:`/`rtl:` direction variants, which correctly target the `dir` + attribute `App.tsx` already sets on ``. Separately, Rolldown flagged 3 dynamic `import()` + calls (`app/transientUiStore.ts`, `services/ai/ecoModeService.ts`, `services/spotlightTour.ts`) + as unable to move their module into a separate chunk because each was also statically imported + elsewhere in the eager bundle — converted all three call sites to plain static imports, removing + the dead-weight async indirection with no bundle-size change. +- **PR #274 review findings that failed to post inline (CodeRabbit/GitHub API error).** A rate-limited + re-review posted its 10 findings as review-body text instead of inline comments; the 2 that did + post (missing `persist-credentials: false`) were already fixed. Of the 7 that only existed in the + review body text: `AiProviderCard`'s status badge now has `role="status" aria-live="polite"`; a + stale in-flight connection-test result (CWE-209) could overwrite `testError` after switching to + Ollama-in-browser — added a monotonic request-id guard in `handleTest` and gated the action-row + error span with the same `!ollamaUntestable` check the paragraph above it already used; + `ollamaService.ts`'s `plugin_unavailable` branch returned `LocalServerError.message` directly (a + public class whose message isn't guaranteed safe for every kind) — now builds its own fixed safe + string, matching the `timeout`/`unreachable` branches; and a scan-endpoint test asserted a loose + `/models` substring instead of the exact `/v1/models` path. The remaining findings (missing + `// QNBS-v3:` comments on several already-commented files/lines; `services/CLAUDE.md`'s + LanguageTool wording) were verified already covered by existing file-level rationale comments or + already fixed in an earlier commit — not re-applied to avoid redundant/stale documentation. - **Ollama / LM Studio / vLLM discovery & connectivity on desktop, CORS noise in the PWA (#266).** Root cause: all local-server traffic (`services/ollamaService.ts`, `scanLocalOpenAiCompatibleEndpoints()`) used the WebView's `fetch`, so inside the Tauri shell the diff --git a/app/listenerMiddleware.ts b/app/listenerMiddleware.ts index 53d003d7..965a20f7 100644 --- a/app/listenerMiddleware.ts +++ b/app/listenerMiddleware.ts @@ -3,6 +3,7 @@ import { createListenerMiddleware, isRejected } from '@reduxjs/toolkit'; import { analyticsActions } from '../features/analytics/analyticsSlice'; import type { ProjectData } from '../features/project/projectSlice'; import { statusActions } from '../features/status/statusSlice'; +import { ecoModeService } from '../services/ai/ecoModeService'; import { extractStoryCodex, saveStoryCodex } from '../services/codexService'; import { checkStorageHealth } from '../services/dbInitialization'; import { @@ -18,6 +19,7 @@ import type { Character, StorySection, World } from '../types'; import { isAnalyticsPersistenceAllowed } from './analyticsGate'; import type { AppDispatch, RootState } from './store'; import { appStoreRef } from './storeRef'; +import { useTransientUiStore } from './transientUiStore'; type ProjectStateWithHistory = { present?: { data?: ProjectData }; @@ -565,9 +567,8 @@ listenerMiddleware.startListening({ }, effect: async (_action, listenerApi) => { const state = listenerApi.getState() as RootState; - const { manuscriptPinnedBinderNodeId, setManuscriptPinnedBinderNodeId } = ( - await import('./transientUiStore') - ).useTransientUiStore.getState(); + const { manuscriptPinnedBinderNodeId, setManuscriptPinnedBinderNodeId } = + useTransientUiStore.getState(); if (!manuscriptPinnedBinderNodeId) return; const nodes = state.project?.present?.data?.binderNodes ?? []; @@ -594,7 +595,6 @@ listenerMiddleware.startListening({ const { setActiveAiMode } = await import('../services/ai/aiModeService'); setActiveAiMode(mode); // QNBS-v3: Bridge eco mode — keeps voice/adaptive-AI/GpuMetricsPanel consumers in sync. - const { ecoModeService } = await import('../services/ai/ecoModeService'); ecoModeService.setAiModeEco(mode === 'eco'); }, }); diff --git a/components/settings/SettingsShared.tsx b/components/settings/SettingsShared.tsx index 4a56dbbf..5f1f60f7 100644 --- a/components/settings/SettingsShared.tsx +++ b/components/settings/SettingsShared.tsx @@ -63,7 +63,7 @@ export const ToggleSwitch: FC<{ {/* QNBS-v3: --sc-surface-base adapts to all themes; bg-white breaks sepia/high-contrast */} {/* Reduced motion: disable transform transition */} diff --git a/services/commands/commandDefinitions.tsx b/services/commands/commandDefinitions.tsx index 164a3b61..d8a7f040 100644 --- a/services/commands/commandDefinitions.tsx +++ b/services/commands/commandDefinitions.tsx @@ -5,6 +5,7 @@ import { projectActions } from '../../features/project/projectSlice'; import { settingsActions } from '../../features/settings/settingsSlice'; import { statusActions } from '../../features/status/statusSlice'; import { isCircuitOpen, resetOpenRouterCircuit } from '../ai/providers/openrouterProvider'; +import { startSpotlightTour } from '../spotlightTour'; import type { CommandDefinition, CommandRuntimeDeps } from './commandTypes'; const SparkIcon = () => ( @@ -632,9 +633,7 @@ export function getStaticCommandDefinitions(): CommandDefinition[] { keywords: ['tour', 'onboarding', 'spotlight'], icon: iconBtn(ICONS.LIGHTNING_BOLT), run: (deps) => { - void import('../../services/spotlightTour').then(({ startSpotlightTour }) => { - startSpotlightTour(deps.t, 'default'); - }); + startSpotlightTour(deps.t, 'default'); }, }, ]; From 1323629184ae2e9c0484021bc8d53da8ffae512a Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:23:57 +0200 Subject: [PATCH 4/6] fix(ai): CodeRabbit findings that failed to post inline on #274 CodeRabbit's re-review hit a GitHub API error posting inline comments and fell back to review-body text for 10 findings; the 2 that did post (persist-credentials) were already fixed. This addresses the 5 remaining real findings among the 7 that never became formal review threads: - AiProviderCard: status badge lacked role="status"/aria-live="polite" for a value that changes between ready/connected/disconnected/browser- unavailable states. - AiProviderCard (CWE-209): a stale in-flight connection-test result could overwrite testError after switching to Ollama-in-browser, since handleTest had no guard against a superseded request completing late. Added a monotonic request-id ref checked before every state update, invalidated whenever the provider/desktop context changes. Also gated the manual-test action row's error span with the same !ollamaUntestable check the paragraph above it already used, so a stale error can't render there either. - ollamaService.ts (CWE-209): the plugin_unavailable branch returned LocalServerError.message directly -- a public class whose message isn't guaranteed safe for every kind -- instead of building its own fixed string like the timeout/unreachable branches already do. - aiProviderService.test.ts: tightened a scan-fallback assertion from a loose '/models' substring to the exact '/v1/models' path. The remaining 2 findings (missing QNBS-v3 rationale comments on several already-commented files; services/CLAUDE.md's LanguageTool wording) were verified already covered by existing file-level comments or already fixed in an earlier commit -- not re-applied to avoid redundant/stale docs. Co-Authored-By: Claude Sonnet 5 --- components/settings/AiProviderCard.tsx | 19 +++++++++++++++++-- services/ollamaService.ts | 9 ++++++++- tests/unit/aiProviderService.test.ts | 4 +++- tests/unit/ollamaService.test.ts | 6 ++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/components/settings/AiProviderCard.tsx b/components/settings/AiProviderCard.tsx index faf99281..7a1543c7 100644 --- a/components/settings/AiProviderCard.tsx +++ b/components/settings/AiProviderCard.tsx @@ -1,6 +1,6 @@ import { ONNX_SUPPORTED_MODELS, WEBLLM_SUPPORTED_MODELS } from '@domain/ai-core'; import type { FC } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from '../../hooks/useTranslation'; import { LOCAL_BACKEND_PRESET_DEFAULT_URL } from '../../services/ai/localBackendPresets'; import type { WebGpuAdapterInfo } from '../../services/ai/webGpuDetectorService'; @@ -55,6 +55,11 @@ export const AiProviderCard: FC = ({ const [scanRows, setScanRows] = useState([]); // QNBS-v3: GPU probe runs once when WebLLM tab is selected — no polling. const [gpuInfo, setGpuInfo] = useState(null); + // QNBS-v3 (CodeRabbit CWE-209): monotonic guard against a stale in-flight test result + // (button click or the auto-test effect) overwriting state after the provider/desktop + // context has since moved on — e.g. a switch to Ollama-in-browser must not let an older + // request's raw error text land in testError once ollamaUntestable becomes true. + const testRequestIdRef = useRef(0); useEffect(() => { storageService @@ -95,6 +100,7 @@ export const AiProviderCard: FC = ({ }, [ollamaBaseUrl]); const handleTest = useCallback(async () => { + const requestId = ++testRequestIdRef.current; setTestStatus('loading'); setTestError(''); try { @@ -102,6 +108,7 @@ export const AiProviderCard: FC = ({ ollamaBaseUrl, openAiCompatibleBaseUrl: advancedAi.openAiCompatibleBaseUrl, }); + if (testRequestIdRef.current !== requestId) return; // stale — superseded by a newer request if (result.ok) { setTestStatus('ok'); } else { @@ -116,6 +123,7 @@ export const AiProviderCard: FC = ({ ); } } catch (e) { + if (testRequestIdRef.current !== requestId) return; // stale — superseded by a newer request setTestStatus('error'); setTestError(e instanceof Error ? e.message : t('settings.ai.unknownError')); } @@ -145,6 +153,9 @@ export const AiProviderCard: FC = ({ ); useEffect(() => { + // QNBS-v3 (CodeRabbit CWE-209): invalidate any in-flight test from the previous provider + // context before deciding whether to start a new one. + testRequestIdRef.current++; // QNBS-v3 (#266): the ollama auto-probe (models + connection test) only runs on desktop. // In the PWA it fired CORS-blocked localhost requests on every settings visit. if (provider === 'ollama' && isDesktop) { @@ -209,6 +220,8 @@ export const AiProviderCard: FC = ({ {t('settings.ai.providerStatusLabel')} = ({ {t('settings.ai.connectionSuccess')} )} - {testStatus === 'error' && ( + {/* QNBS-v3 (CodeRabbit CWE-209): mirror the guard above — a stale in-flight test for a + prior provider must not surface raw error text once Ollama-in-browser is selected. */} + {!ollamaUntestable && testStatus === 'error' && (