diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 91c59331..443a14ef 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -63,7 +63,7 @@ types.ts → Core shared interfaces and types 9. **Voice Full Support:** Gated behind `featureFlags.enableVoiceSupport` + `settings.voice.enabled`. Abstract engine pattern in `services/voice/voiceTypes.ts` (SttEngine, TtsEngine, VadEngine, WakeWordEngine, IntentEngine). `VoiceCommandService` singleton manages state machine (idle → listening → processing → speaking). Web Speech API fallbacks require zero downloads. Hooks: `useVoice`, `usePushToTalk` (Ctrl+Shift+V), `useVoiceDictation`. -10. **Feature Flags:** **23 flags** in `features/featureFlags/featureFlagsSlice.ts`. New installs get the **full feature set** — all default **on** except five opt-in flags that default **off**: `enableRtlLayout`, `enableVoiceSupport`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync`. (`enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core; `enablePlotBoardV2` + `enableCloudSync` were retired — none remain in the slice.) See `docs/FEATURE-PARITY.md`. Do not use scattered `if (true)` hacks — all experimental features must go through a flag. +10. **Feature Flags:** **22 flags** in `features/featureFlags/featureFlagsSlice.ts`. New installs get the **full feature set** — all default **on** except six opt-in flags that default **off**: `enableRtlLayout`, `enableVoiceSupport`, `enableProForge`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync`. (`enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core; `enablePlotBoardV2` + `enableCloudSync` were retired — none remain in the slice.) See `docs/FEATURE-PARITY.md`. Do not use scattered `if (true)` hacks — all experimental features must go through a flag. 11. **Global AI Copilot (v2):** `enableGlobalCopilot` flag. `CopilotPanel` (dialog/sidebar mode), `CopilotMessageList` (markdown rendering via DOMPurify), `InlineAnnotationLayer` (badge in ManuscriptEditor). Heuristic rules: `services/copilot/heuristicEngine.ts` (8 rules). Apply-to-chapter: `services/copilot/actionApplier.ts` (offset-safe edit, redux-undo, ≥70% length gate). ProForge integration: Ask-Copilot chip on each `ReviewItemCard`. Docs: `docs/COPILOT.md`, `docs/HEURISTIC-RULES.md`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b947d85..08974cb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,8 +45,24 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/setup - - name: pnpm audit (high + critical) - run: pnpm audit --audit-level=high + # QNBS-v3: pnpm's audit client cannot decode force-gzipped responses from some npm CDN edge + # nodes (ERR_PNPM_AUDIT_BAD_RESPONSE, deterministic per edge — retries are useless). The + # enforced vulnerability gate in this job is the OSV scan below (covers BOTH pnpm-lock.yaml + # and Cargo.lock with a superset DB); this step is advisory-only until pnpm fixes decoding. + - name: pnpm audit (high + critical, advisory) + continue-on-error: true + run: | + if pnpm audit --audit-level=high; then + echo "pnpm audit: clean" + else + echo "::warning::pnpm audit failed (registry gzip-decoding bug or findings) — OSV scan below is the enforced gate" + exit 1 + fi + + # QNBS-v3 (#60): vendored y-webrtc fork invariant guard (PBKDF2 600k, non-extractable keys, + # DataChannel encryption, no dangling upstream dep) — fails the build on fork drift. + - name: Vendor fork invariant guard + run: pnpm run verify:vendor # QNBS-v3: osv-scanner.toml (in src-tauri/) suppresses accepted RUSTSEC advisories for Cargo.lock; # scan both lockfiles so npm + Rust advisories are caught on every push diff --git a/AGENTS.md b/AGENTS.md index b1ab2a67..e31b362b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ - **Version:** `1.23.0` - **License:** MIT -The app supports a multi-provider AI stack (Gemini, OpenAI, Claude, Grok, OpenRouter, Ollama, WebLLM, ONNX Runtime Web, Transformers.js), four AI execution modes (Hybrid / Cloud / Local / Eco), real-time collaboration with E2E encryption, a Plot Board v2 with swimlane/canvas/timeline modes, character/world management, manuscript export, voice dictation, and an 11-locale i18n layer. +The app supports a multi-provider AI stack (Gemini, OpenAI, Claude, Grok, OpenRouter, Ollama, WebLLM, ONNX Runtime Web, Transformers.js), four AI execution modes (Hybrid / Cloud / Local / Eco), real-time collaboration with E2E encryption, a Plot Board v2 with swimlane/canvas/timeline modes, character/world management, manuscript export, voice dictation, and a 19-locale i18n layer. --- @@ -84,7 +84,7 @@ WorldScript-Studio/ │ ├── status/ # App-wide status / loading flags │ ├── writer/ # Writer view state │ ├── versionControl/ # Snapshots and branches -│ ├── featureFlags/ # 23 flags — full set on by default; 5 opt-in (default-off) +│ ├── featureFlags/ # 22 flags — full set on by default; 6 opt-in (default-off) │ ├── plotBoard/ # Ephemeral viewport/draw state (NOT undo-able; localStorage) │ ├── progressTracker/ # Writing sessions, streaks, goals │ ├── sceneComments/ # Per-scene comments (EntityAdapter) @@ -114,7 +114,7 @@ WorldScript-Studio/ │ ├── collab-transport/ # Vendor fork of y-webrtc 10.3.0 with E2E encryption patch │ ├── ui/ # Tailwind preset + design tokens │ └── worker-bus/ # Typed worker pool, circuit breakers, dead-letter queue -├── locales/ # i18n source JSON modules (11 locales) +├── locales/ # i18n source JSON modules (19 locales) ├── public/ # Static assets; runtime i18n bundles `public/locales//bundle.json` ├── tests/ │ ├── unit/ # Vitest tests (co-located naming convention) @@ -387,7 +387,7 @@ Edge builds run `scripts/build-edge.mjs` which sets `DEPLOY_TARGET=edge` and pat ### Feature Flags -- `features/featureFlags/featureFlagsSlice.ts` gates **23 flags**. New installs get the **full feature set**: all default **on** except five opt-in flags that default **off** — `enableRtlLayout`, `enableVoiceSupport`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync`. (`enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core; `enablePlotBoardV2` + `enableCloudSync` were retired — none remain in the slice.) See `docs/FEATURE-PARITY.md` for the per-flag matrix. +- `features/featureFlags/featureFlagsSlice.ts` gates **22 flags**. New installs get the **full feature set**: all default **on** except six opt-in flags that default **off** — `enableRtlLayout`, `enableVoiceSupport`, `enableProForge`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync`. (`enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core; `enablePlotBoardV2` + `enableCloudSync` were retired — none remain in the slice.) See `docs/FEATURE-PARITY.md` for the per-flag matrix. - UI: Settings → Experimental flags. - Do not use scattered `if (true)` hacks. @@ -486,10 +486,10 @@ Central orchestration layer for all background worker tasks. Messages use short ## Internationalization (i18n) - **Custom React Context** (`contexts/I18nContext.tsx`) — not i18next. -- Source modules: `locales//*.json` for **11 locales**: `ar`, `de`, `el`, `en`, `es`, `fr`, `he`, `it`, `ja`, `pt`, `zh`. +- Source modules: `locales//*.json` for **19 locales**: `ar`, `de`, `el`, `en`, `es`, `eu`, `fa`, `fi`, `fr`, `he`, `hu`, `is`, `it`, `ja`, `ko`, `pt`, `ru`, `sv`, `zh`. - Runtime bundles: `public/locales//bundle.json` (rebuilt by `pnpm run i18n:bundle` or automatically via `predev` / `prebuild`). - Hook: `useTranslation()` returns `t('key.path')`. **No hardcoded text** in UI. -- Key parity is enforced in CI (`pnpm run i18n:check`). Add keys to **all eleven** locale trees. +- Key parity is enforced in CI (`pnpm run i18n:check`). Add keys to **all nineteen** locale trees. - Repair scripts: `services/i18nBootstrap.ts` and `services/i18nRepair.ts` handle missing keys / bundle corruption. - RTL Beta: Arabic (`ar`) and Hebrew (`he`) set `html[dir="rtl"]`. Layout mirroring uses logical properties and a global `[dir="rtl"]` CSS net. Canvas/SVG boards (Plot Board, Character Graph) stay LTR to keep pointer/geometry math correct. `enableRtlLayout` flag forces RTL for testing. diff --git a/CHANGELOG.md b/CHANGELOG.md index 568c6a99..d054e580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Settings hygiene.** Removed stale Experimental-category search hints (`plot board`, `codex`, + `cross project` — retired/promoted flags) in favor of current features; `ProForgeDashboard` now + uses the catalog-driven `MaturityBadge` instead of a hard-coded Experimental pill, keeping the + v1.24 maturity-label convention consistent. + - **Desktop settings "minimize to tray" is now a proper switch.** `DesktopSection` used the only raw `` left in Settings, with its hint not programmatically associated. It now renders the design-system `ToggleSwitch` (`role="switch"`, `aria-labelledby` + `aria-describedby` @@ -90,7 +95,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 provider is still described as an adaptive-engine backend). Refreshed in the five Production locales. - **ProForge is now opt-in (default off).** The experimental 8-stage agentic editing pipeline (`enableProForge`) shipped on by default; it is token-heavy and carries loop risk, so it is now a - user opt-in like Voice and the Global Copilot. New installs get **17 default-on / 6 default-off** + user opt-in like Voice and the Global Copilot. New installs get **16 default-on / 6 default-off** flags. Existing users who enabled or relied on it are unaffected (the persisted value wins); everyone can still turn it on under Settings → Experimental. - **Settings → Experimental features are grouped by category** (Writing, AI, Editing Pipeline, @@ -105,11 +110,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **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 + cross-origin `localhost` requests died on CORS/PNA, and in the PWA the settings auto-effect probed + `localhost:11434` on every visit (loud CORS console errors). All local-server calls now go through + the new thin `services/localServerHttp.ts`, which routes via `@tauri-apps/plugin-http` (native, + CORS-free) on desktop and keeps browser `fetch` on the web, with shared URL normalization, timeout + composition and `unreachable`/`timeout` classification (user aborts still propagate unchanged). + The Settings → AI card no longer auto-probes localhost in the PWA — it shows a quiet "desktop app + required" banner with a download CTA instead — and the desktop scan gained per-endpoint status + badges (reachable / no response / timeout / HTTP error) plus a one-click **Use this URL** action. + All new strings localized in 19 locales. See + [ADR 0012](docs/adr/0012-local-server-connectivity-tauri-http.md) and + [`docs/LOCAL-AI.md`](docs/LOCAL-AI.md). + - **Feature-catalog / slice default drift made structurally impossible.** `features/featureCatalog.ts` - now covers all **23** flags (was 16) and **derives** each entry's `defaultOn` from the slice's + now covers all **22** flags (was 16) and **derives** each entry's `defaultOn` from the slice's `defaultFeatureFlagsState` instead of hand-keying it — the class of bug where the catalog said `false` while the slice said `true` for ~12 flags can no longer recur (guarded by the new `tests/unit/featureCatalog.test.ts`). Added risk-level / desktop-requirement / dependency metadata. + +### Security + +- **Tauri HTTP-plugin capability scope pinned (#266).** `http:default` alone grants **no** URL + scope — every plugin-http call (including AI-SDK cloud calls on desktop) was silently denied by + the plugin's allow-list check. The capability now explicitly allows loopback any-port + (`http://localhost:*/*`, `http://127.0.0.1:*/*` — Ollama/LM Studio/vLLM + custom ports) and the + cloud endpoints mirroring the Tauri CSP `connect-src` (Gemini, OpenAI, x.ai, OpenRouter, Groq). +- **y-webrtc vendor-fork audit + CI invariant guard (#60).** Full-file diff of + `packages/collab-transport` against upstream `y-webrtc@10.3.0`: no deviations beyond the three + documented SC patches (PBKDF2 600k iterations, `extractable: false`, `return promise.reject`) plus + the DataChannel E2E encryption — recorded in `packages/collab-transport/AUDIT.md`, fork bumped to + `10.3.0-sc2`. The deprecated `patches/y-webrtc@10.3.0.patch` and the dead root `y-webrtc` + dependency (zero imports) are removed, and the previously-referenced-but-missing + `scripts/verify-vendor-fork.mjs` now exists and runs in the CI security job (`verify:vendor`). +- **Advisory batch remediated (2026-07-26, CI security gate).** Cargo.lock: `quick-xml` 0.39.4 → + 0.41.0 via `plist` 1.10.0 (RUSTSEC-2026-0194/0195, CVSS 7.5), `anyhow` → 1.0.104, + `crossbeam-epoch` → 0.9.20, `serde_with` → 3.21.0. pnpm overrides raised: `dompurify` → 3.4.12, + `protobufjs` → 8.7.1, `body-parser` → 1.20.6 (bounded `<2` for express-4 compat). The two stale + no-longer-matching OSV ignores (dompurify GHSA-x4vx-rjvf-j5p4, js-yaml-3.x GHSA-h67p-54hq-rp68) + were dropped from `src-tauri/osv-scanner.toml`. The `pnpm audit` CI step is now advisory + (`continue-on-error`): some npm CDN edge nodes force-gzip the bulk-advisories response which + pnpm cannot decode (`ERR_PNPM_AUDIT_BAD_RESPONSE`, deterministic per edge — retries useless); + the OSV scan of both lockfiles remains the enforced gate. + ### Removed - **Dead `enableWebnnInference` feature flag removed.** The flag shipped default-on but **no runtime diff --git a/CLAUDE.md b/CLAUDE.md index 529516c6..22d34899 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -246,7 +246,7 @@ Production uses **rolldown** (not esbuild/rollup); CI E2E runs `vite dev` — pr ### Feature Flags -Experimental features are gated behind `features/featureFlags/featureFlagsSlice.ts` (**23 flags**). New installs get the **full feature set**: all flags default **on** except six user-opt-in flags. `enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core behaviour (v1.20 / v1.8); `enablePlotBoardV2` and `enableCloudSync` were retired — none of those four remain in the slice. UI: Settings → Experimental flags (`FeatureFlagsSection.tsx`). Do not use scattered `if (true)` hacks. +Experimental features are gated behind `features/featureFlags/featureFlagsSlice.ts` (**22 flags**). New installs get the **full feature set**: all flags default **on** except six user-opt-in flags. `enableCodexAutoTracking` + `enableCrossProjectSearch` were promoted to permanent core behaviour (v1.20 / v1.8); `enablePlotBoardV2` and `enableCloudSync` were retired — none of those four remain in the slice. UI: Settings → Experimental flags (`FeatureFlagsSection.tsx`). Do not use scattered `if (true)` hacks. **Default on (17):** `enableStoryBibleAdvanced`, `enableBinderResearch`, `enableCompileWizard`, `enableProjectHealthScore`, `enableAppHealthPanel`, `enableDuckDbAnalytics`, `enableObjectsGroups`, `enableMindMaps`, `enableCharacterInterviews`, `enableLoraAdapters`, `enablePluginSystem`, `enableIdbAtRestEncryption` (B-1, passphrase UX complete — Settings › Privacy), `enableAdaptiveAiEngine`, `enableWebnnInference`, `enableComputeShaders`, `enableWorkerBusV2`, `enableRustCompute`. **User opt-in — default off (6):** `enableProForge` (experimental, token-heavy 8-stage agentic pipeline — flipped to opt-in in v1.24 post-release), `enableVoiceSupport` (requires browser mic permission), `enableVoiceWasm` (B-2, ~57 MB Whisper download), `enableGlobalCopilot` (ambient AI), `enableRtlLayout` (B-5, ar/he stubs only), `enableLocalFirstSync` (shadow Yjs projection, ADR-0008; Redux stays SoT). The Settings UI groups these by catalog tier; `features/featureCatalog.ts` **derives** each flag's `defaultOn` from the slice (no hand-keyed drift). Note: `enableCloudSync` was **retired** in v1.20 (no UI shipped; `CloudSyncBackend.create()` requires explicit-consent boolean instead). @@ -337,7 +337,7 @@ All `.md` guides listed in **[`README.md`](README.md#-documentation-hub) § Docu **useAppSelectorShallow with plotBoard:** Include `plotBoard: { activeMode: 'swimlane', snapToGrid: false, selectedConnectionId: null, isDrawingConnection: false, drawFromSectionId: null, activeSubplotFilter: null, zoom: 1, panX: 0, panY: 0 }` in mock state. Connections/subplots/tensionOverrides are in `project.present.data`. Add `// biome-ignore lint/suspicious/noExplicitAny: test mock` before `(selector: (s: any) => unknown)` lines. -**FeatureFlagsState mocks:** Always include ALL 23 flags (TypeScript strict rejects partial). Only **six** default **off**: `enableProForge`, `enableRtlLayout`, `enableVoiceSupport`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync` — every other flag (including all edge-AI flags and `enableIdbAtRestEncryption`) defaults **on**. When a test needs a non-default flag state, set it explicitly in the mock — don't assume the default. +**FeatureFlagsState mocks:** Always include ALL 22 flags (TypeScript strict rejects partial). Only **six** default **off**: `enableProForge`, `enableRtlLayout`, `enableVoiceSupport`, `enableVoiceWasm`, `enableGlobalCopilot`, `enableLocalFirstSync` — every other flag (including all edge-AI flags and `enableIdbAtRestEncryption`) defaults **on**. When a test needs a non-default flag state, set it explicitly in the mock — don't assume the default. **ConnectionLayer test IDs:** `data-testid="connection-group"` — query by testid, not role. diff --git a/VENDOR-FORKS.md b/VENDOR-FORKS.md index 2fd09f22..0b2572ee 100644 --- a/VENDOR-FORKS.md +++ b/VENDOR-FORKS.md @@ -8,19 +8,24 @@ upstream monitoring and patch porting. | Attribute | Value | |-----------|-------| | **Upstream** | `y-webrtc@10.3.0` | -| **Fork version** | `10.3.0-sc1` | +| **Fork version** | `10.3.0-sc2` | | **Reason for fork** | E2E encryption (AES-256-GCM via Web Crypto) is not available upstream. The fork adds PBKDF2 key derivation, non-extractable CryptoKeys, and encrypted WebRTC DataChannels compatible with the y-webrtc signalling protocol. | | **Critical invariants** | 1. PBKDF2 iterations must remain **600,000** (OWASP 2024 SHA-256 minimum).
2. All derived `CryptoKey` instances must have `extractable: false`.
3. `promise.reject()` in `decrypt()` must be prefixed with `return`. | -| **Update protocol** | 1. Watch upstream releases via GitHub API or Renovate (excluded in config).
2. On new upstream release, create a diff branch `vendor/y-webrtc-`.
3. Port the three encryption patches manually; run `packages/collab-transport` test suite.
4. Update `VENDOR-DIFF.md` baseline and bump the `-scN` suffix. | +| **Update protocol** | 1. Watch upstream releases via GitHub API or Renovate (excluded in config).
2. On new upstream release, create a diff branch `vendor/y-webrtc-`.
3. Port the three encryption patches manually; run `packages/collab-transport` test suite.
4. Update `packages/collab-transport/AUDIT.md` and bump the `-scN` suffix. | | **Owner** | qnbs | | **Issue tracker** | #60 | +## Audit log + +| Date | Auditor | Result | +|------|---------|--------| +| 2026-07-25 | Kimi Code | Full-file diff vs upstream `10.3.0`: no deviations beyond the three SC patches. Deprecated `patches/y-webrtc@10.3.0.patch` + dead root dependency removed; `verify-vendor-fork.mjs` created and wired into CI (`verify:vendor`); fork bumped to `10.3.0-sc2`. Details: `packages/collab-transport/AUDIT.md`. | + ## `patches/y-webrtc@10.3.0.patch` | Attribute | Value | |-----------|-------| -| **Status** | **DEPRECATED** — the vendor fork above supersedes this patch. | -| **Action** | Safe to remove once the fork is confirmed stable in production (v1.19.0+). | +| **Status** | **REMOVED** (2026-07-25, #60 audit) — the vendor fork above superseded this patch in v1.19.0. | ## Audit automation @@ -28,9 +33,11 @@ upstream monitoring and patch porting. - Asserts PBKDF2 iterations == 600k - Asserts `extractable: false` - Asserts `return promise.reject(...)` -- Asserts version string matches expected `-scN` suffix +- Asserts DataChannel encrypt/decrypt wiring, `-scN` version suffix, and no dangling upstream dep/patch -Wired into CI security job as `verify:vendor`. +Wired into the CI security job as `verify:vendor` (created 2026-07-25 during the #60 audit). +The per-release diff record lives in `packages/collab-transport/AUDIT.md` (replaces the formerly +planned `VENDOR-DIFF.md`). ## CVE / advisory monitoring (audit finding F-7) @@ -52,7 +59,7 @@ see advisories against it. Therefore, on every fork-maintenance review (and at m and the project's release notes — for anything at or below our base `10.3.0`. 2. If a relevant advisory exists, port the upstream fix into the vendored `src/` alongside the three encryption patches, run the `packages/collab-transport` test suite + `pnpm run verify:vendor`, - bump the `-scN` suffix, and record it in `VENDOR-DIFF.md`. + bump the `-scN` suffix, and record it in `packages/collab-transport/AUDIT.md`. 3. The fork's npm dependencies need no manual step — rely on the OSV `pnpm-lock.yaml` scan + Dependabot. > Coverage policy for unit tests lives in [`docs/COVERAGE-POLICY.md`](docs/COVERAGE-POLICY.md). diff --git a/components/proForge/ProForgeDashboard.tsx b/components/proForge/ProForgeDashboard.tsx index 846b4e47..ee50514a 100644 --- a/components/proForge/ProForgeDashboard.tsx +++ b/components/proForge/ProForgeDashboard.tsx @@ -7,7 +7,7 @@ import type React from 'react'; import { useCallback, useState } from 'react'; import { useProForgeViewContext } from '../../contexts/ProForgeViewContext'; import { PIPELINE_STAGES, type PipelineStage } from '../../features/proForge/types'; -import { Badge } from '../ui/Badge'; +import { MaturityBadge } from '../settings/MaturityBadge'; import { PipelineProgressPanel } from './PipelineProgressPanel'; import { PipelineReviewPanel } from './PipelineReviewPanel'; @@ -88,8 +88,9 @@ export const ProForgeDashboard: React.FC = () => {

{t('proforge.pipeline.title')}

- {/* QNBS-v3: ProForge is an on-by-default but experimental agentic pipeline — label it. */} - {t('common.badge.experimental')} + {/* QNBS-v3: ProForge is opt-in + experimental — label driven by FEATURE_CATALOG so it + can never drift from the catalog/slice (MaturityBadge convention, v1.24). */} +
{/* QNBS-v3: PR6 — make the human-in-the-loop expectation explicit, not just implied in help. */}

diff --git a/components/settings/AiProviderCard.tsx b/components/settings/AiProviderCard.tsx index c0662714..dc91c822 100644 --- a/components/settings/AiProviderCard.tsx +++ b/components/settings/AiProviderCard.tsx @@ -6,6 +6,7 @@ import { LOCAL_BACKEND_PRESET_DEFAULT_URL } from '../../services/ai/localBackend import type { WebGpuAdapterInfo } from '../../services/ai/webGpuDetectorService'; import { detectWebGpuDetails } from '../../services/ai/webGpuDetectorService'; import { + type LocalEndpointScanResult, listOllamaModels, scanLocalOpenAiCompatibleEndpoints, testAIConnection, @@ -46,9 +47,7 @@ export const AiProviderCard: FC = ({ const [isLoadingModels, setIsLoadingModels] = useState(false); const [isSavingKey, setIsSavingKey] = useState(false); const [scanBusy, setScanBusy] = useState(false); - const [scanRows, setScanRows] = useState<{ labelKey: string; baseUrl: string; ok: boolean }[]>( - [], - ); + const [scanRows, setScanRows] = useState([]); // QNBS-v3: GPU probe runs once when WebLLM tab is selected — no polling. const [gpuInfo, setGpuInfo] = useState(null); @@ -73,6 +72,12 @@ export const AiProviderCard: FC = ({ }, [openaiKey]); const handleLoadOllamaModels = useCallback(async () => { + // QNBS-v3 (#266): never probe localhost from the PWA — the request is CORS/PNA-blocked anyway + // and only produces console noise. Local servers are a desktop-only feature by policy. + if (!isTauriRuntime()) { + setOllamaModels([]); + return; + } setIsLoadingModels(true); try { const models = await listOllamaModels(ollamaBaseUrl); @@ -107,15 +112,30 @@ export const AiProviderCard: FC = ({ const handleScanLocals = useCallback(async () => { setScanBusy(true); try { - const rows = await scanLocalOpenAiCompatibleEndpoints(); - setScanRows(rows.map((r) => ({ labelKey: r.labelKey, baseUrl: r.baseUrl, ok: r.ok }))); + setScanRows(await scanLocalOpenAiCompatibleEndpoints()); } finally { setScanBusy(false); } }, []); + // QNBS-v3 (#266): one-click adopt of a discovered server URL (+ matching preset when known). + const handleUseScannedUrl = useCallback( + (baseUrl: string) => { + const presetEntry = ( + Object.entries(LOCAL_BACKEND_PRESET_DEFAULT_URL) as [LocalBackendPreset, string][] + ).find(([preset, url]) => preset !== 'custom' && url === baseUrl); + onAdvancedAiPatch({ + ollamaBaseUrl: baseUrl, + localBackendPreset: presetEntry ? presetEntry[0] : 'custom', + }); + }, + [onAdvancedAiPatch], + ); + useEffect(() => { - if (provider === 'ollama') { + // 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) { void handleLoadOllamaModels(); void handleTest(); } else if (provider === 'webllm') { @@ -125,7 +145,7 @@ export const AiProviderCard: FC = ({ .then(setGpuInfo) .catch(() => setGpuInfo({ status: 'unknown' })); } - }, [provider, handleLoadOllamaModels, handleTest]); + }, [provider, isDesktop, handleLoadOllamaModels, handleTest]); const providers: { id: AIProvider; label: string }[] = [ { id: 'gemini', label: 'Google Gemini' }, @@ -272,12 +292,23 @@ export const AiProviderCard: FC = ({ {provider === 'ollama' && (

{!isDesktop && ( -
-

+ // QNBS-v3 (#266): quiet banner instead of CORS console noise — the PWA never + // auto-probes localhost, so this is the only place the restriction surfaces. +

+

-

{t('settings.ai.ollamaBrowserNote')}

+

{t('settings.ai.ollamaDesktopOnlyBody')}

+ +
)} {isDesktop && ( @@ -327,7 +358,7 @@ export const AiProviderCard: FC = ({ /> {scanRows.length > 0 && ( -
    +
      {scanRows.map((row) => ( -
    • - {t(row.labelKey)} — {row.baseUrl}{' '} +
    • + + {t(row.labelKey)} — {row.baseUrl} + - {row.ok ? t('settings.ai.scanOk') : t('settings.ai.scanNo')} + {row.state === 'ok' && t('settings.ai.scanOk')} + {row.state === 'unreachable' && t('settings.ai.scanNo')} + {row.state === 'timeout' && t('settings.ai.scanStateTimeout')} + {row.state === 'http' && + `${t('settings.ai.scanStateHttp')}${row.status ? ` (${row.status})` : ''}`} + {row.ok && ( + + )}
    • ))}
    @@ -513,7 +563,13 @@ export const AiProviderCard: FC = ({ {provider !== 'gemini' && (
    -