Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false

- uses: ./.github/actions/setup

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/storybook-debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- uses: ./.github/actions/setup

- name: Build Storybook
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/voice-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## 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.
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<html>`. 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
Expand Down Expand Up @@ -171,6 +195,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
Expand Down
11 changes: 11 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions app/listenerMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 };
Expand Down Expand Up @@ -565,9 +567,11 @@ listenerMiddleware.startListening({
},
effect: async (_action, listenerApi) => {
const state = listenerApi.getState() as RootState;
const { manuscriptPinnedBinderNodeId, setManuscriptPinnedBinderNodeId } = (
await import('./transientUiStore')
).useTransientUiStore.getState();
// QNBS-v3: static import — transientUiStore is already statically imported by many components
// (BinderPanel, ExportView, etc.), so the prior dynamic import() never actually code-split it;
// Rolldown flagged it as dead weight with no chunking benefit.
const { manuscriptPinnedBinderNodeId, setManuscriptPinnedBinderNodeId } =
useTransientUiStore.getState();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!manuscriptPinnedBinderNodeId) return;

const nodes = state.project?.present?.data?.binderNodes ?? [];
Expand All @@ -594,7 +598,9 @@ 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');
// Static import (unlike aiModeService above): ecoModeService is already statically imported
// elsewhere (GpuMetricsPanel, useAdaptiveAi, useVoice), so its own dynamic import() never
// actually code-split it — Rolldown flagged it as dead weight with no chunking benefit.
ecoModeService.setAiModeEco(mode === 'eco');
},
});
Expand Down
19 changes: 17 additions & 2 deletions components/settings/AiProviderCard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -55,6 +55,11 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
const [scanRows, setScanRows] = useState<LocalEndpointScanResult[]>([]);
// QNBS-v3: GPU probe runs once when WebLLM tab is selected — no polling.
const [gpuInfo, setGpuInfo] = useState<WebGpuAdapterInfo | null>(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
Expand Down Expand Up @@ -95,13 +100,15 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
}, [ollamaBaseUrl]);

const handleTest = useCallback(async () => {
const requestId = ++testRequestIdRef.current;
setTestStatus('loading');
setTestError('');
try {
const result = await testAIConnection(provider, {
ollamaBaseUrl,
openAiCompatibleBaseUrl: advancedAi.openAiCompatibleBaseUrl,
});
if (testRequestIdRef.current !== requestId) return; // stale — superseded by a newer request
if (result.ok) {
setTestStatus('ok');
} else {
Expand All @@ -116,6 +123,7 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
);
}
} 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'));
}
Expand Down Expand Up @@ -145,6 +153,9 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
);

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) {
Expand Down Expand Up @@ -209,6 +220,8 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
{t('settings.ai.providerStatusLabel')}
</span>
<span
role="status"
aria-live="polite"
className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold ${
ollamaUntestable
? 'bg-[var(--sc-surface-overlay)] text-[var(--sc-text-secondary)]'
Expand Down Expand Up @@ -602,7 +615,9 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
{t('settings.ai.connectionSuccess')}
</span>
)}
{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' && (
<span className="text-sm text-[var(--sc-danger-fg)] flex items-center gap-1">
<Icon name="error" size="sm" aria-hidden="true" />
{testError}
Expand Down
Loading
Loading