Skip to content

feat(ai): native Grok provider wiring (ADR-0016 Phase 1) - #299

Merged
qnbs merged 5 commits into
mainfrom
feat/grok-native-provider
Jul 30, 2026
Merged

feat(ai): native Grok provider wiring (ADR-0016 Phase 1)#299
qnbs merged 5 commits into
mainfrom
feat/grok-native-provider

Conversation

@qnbs

@qnbsqnbs commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

PR 1 of 4 in a stacked sequence (this one → Claude Track A → Claude Track B → Ollama browser opt-in). Split out of the original combined PR #298 after CodeRabbit reported it exceeds its own 150-file review limit (204 files) — see the closing comment on #298.

  • Grok (xAI) wired into AiProviderCard.tsx's primary provider dropdown with its own key input + model selector (grok-3/grok-3-mini), backed by the already-working streamGrok(). Added a case 'grok' to providerFactory.ts's providerToKind() so the newer Vercel-AI-SDK Writer-streaming path gets it too.
  • Also fixed the whole provider-selector array's i18n (several labels were hardcoded literals, not t() calls).
  • Includes 3 minor CodeRabbit findings deferred from PR feat(ci,app): ci-success aggregator + typecheck boundary gaps + Grok/Claude plan (WS-3+WS-6) #297 (plan-doc numbering, missing QNBS-v3 comments, a weak test assertion) and the Phase 0 re-verification + ADR-0016 this whole sequence is built on.
  • i18n housekeeping note: the original combined commit's check-i18n-keys.mjs --fix run incidentally re-sorted keys in ~116 unrelated locale files (zero content change, pure key-order noise) — those are excluded from this PR's diff; only the files with real content changes (new settings.* keys × 19 locales + rebuilt bundles) are included, keeping this PR at 50 files.

Test plan

  • pnpm run lint / pnpm run typecheck — clean
  • pnpm run i18n:check — 19 locales × 2855 keys
  • pnpm run docs:check — clean
  • pnpm exec tsx scripts/audit-feature-parity.ts — 22 flags, 0 drifts (Grok isn't a feature flag)
  • node scripts/audit-tokens.mjs — 160 ≤ baseline 160
  • providerFactory.test.ts, worldScriptCompletionFetch.test.ts, AiProviderCard.test.tsx, listenerMiddleware.test.ts — 74 tests green
  • CI — awaiting push

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added xAI Grok as a selectable AI provider.
    • Added Grok API-key management, model selection, and connection settings.
    • Enabled Grok-powered world scripting with automatic model fallback.
    • Expanded localized provider labels and Grok settings text across supported languages.
  • Documentation

    • Updated provider integration plans and architecture documentation.
    • Refreshed documented localization key totals.
  • Tests

    • Added coverage for Grok configuration, routing, authentication errors, and model defaults.
    • Improved local-first synchronization test coverage.

qnbs added 4 commits July 30, 2026 21:40
Four findings deferred from PR #297 to keep that PR's merge unblocked,
addressed here first per explicit instruction before starting the
Grok/Claude/Ollama plan execution:
1. GROK-PROVIDER-INTEGRATION-PLAN.md: the addendum was numbered "## 7"
but its own subsections said "8.1"-"8.4", and a "§§1-7 above"
self-reference included the addendum itself. Renumbered to 7.1-7.4,
fixed both self-references, and fixed a second pre-existing
ambiguous "§5" cross-reference (meant "item 5 within Phase 1's own
list", not top-level section 5) while in the area.
2. tests/unit/listenerMiddleware.test.ts: added the required QNBS-v3
comment next to the new featureFlagsActions import.
3. Added the required QNBS-v3 comment before the new "local-first
shadow sync" describe block.
4. Rewrote the local-first sync test to assert the actual
selector-to-binding boundary directly instead of inferring success
from an absence of logger calls in an unmocked-dependency,
environment-dependent setup. Added real mocks for
services/localFirst/{projectDoc,docBinding,docPersistence} and
services/storage/storageEncryptionService so the test can assert
the exact object selectProjectData(state) returns is what reaches
ProjectDocBinding's constructor and syncFromProject/verify.
Hit two real bugs while building this: (a) an arrow-function mock
implementation can never be used as a constructor -- switched to a
real class, which Biome's useArrowFunction rule doesn't flag either
(a function-expression fix would have been immediately reverted by
the same linter that was satisfied by removing the arrow function
in the first place); (b) vi.fn().mockImplementation() can't type a
class constructor against its generic (...args) => any signature --
mocked the class directly instead of wrapping it in vi.fn().
Re-checked the plan's own findings against current main before starting
implementation (Phase 0's explicit purpose), and found the original
draft's Claude analysis was incomplete in a way that changes the whole
approach:
- AiProviderCard.tsx's primary provider dropdown already has an
'anthropic' entry with a dedicated warning block -- missed originally
because the investigation searched for the literal string "claude",
never "anthropic" (the actual identifier), when checking that file.
Its existing i18n copy already half-promises desktop support
("...or use the Tauri desktop app for Anthropic calls").
- streamAnthropic() throws unconditionally on EVERY platform, including
desktop -- but CORS is a browser-only restriction. It never even
checks isTauriRuntime() before throwing. Tauri's native HTTP plugin
(@tauri-apps/plugin-http, services/localServerHttp.ts) already solves
exactly this class of problem for Ollama/LM Studio/vLLM (ADR-0012);
the same escape hatch works for any HTTPS endpoint, including
api.anthropic.com, since native networking isn't subject to browser
CORS at all.
Restructured Phase 2 into two independent tracks: Track A (desktop) is
a narrow bug fix reusing ADR-0012's established pattern, no new
infrastructure, ships first. Track B (web/PWA) is the actual new
architecture -- the serverless proxy -- and is now correctly scoped to
only the three web deploy targets (desktop no longer needs it at all).
Updated the Executive Summary, both Definition of Done checklists, and
all internal cross-references accordingly.
Formalizes GROK-PROVIDER-INTEGRATION-PLAN.md's Phase 0 decision record:
Grok is a pure UI-wiring fix (backend already works); Claude splits
into Track A (desktop, native-HTTP via the ADR-0012 pattern, ships
first, no new infrastructure) and Track B (web/PWA, this app's first
serverless backend dependency, ships second). Also documents why the
Ollama-in-PWA follow-up (Issue #266) is a separate decision, not a
Track-B variant -- a hosted proxy can't reach a user's own localhost.
Grok (xAI, Cloud 4) had a real, working backend (aiProviderService.ts's
streamGrok(), key storage, a live /v1/models connection test) but was
never selectable as a primary provider -- reachable only as a hybrid-
fallback-chain option. This closes that UI/wiring gap; no new backend
work needed.
- AiProviderCard.tsx: added Grok to the primary provider dropdown with
its own API-key input + model selector (grok-3 / grok-3-mini),
reusing the existing storageService key-storage plumbing and the
already-working testAIConnection() grok case for Test Connection.
Also fixed the whole provider array's i18n while touching it --
gemini/openai/ollama/anthropic labels were hardcoded literals too,
not just Grok's gap.
- providerFactory.ts: providerToKind() gets a distinct 'grok' kind
(not folded into 'openaiCompatible') -- Grok needs a fixed baseURL
(https://api.x.ai/v1) and a real stored key, unlike the Ollama
default that also maps to 'openaiCompatible'; folding it in would
have silently sent Grok requests through Ollama's localhost default.
- worldScriptCompletionFetch.ts: resolveModelConfig() branches on the
new 'grok' kind, bringing Grok into the newer Writer-streaming path
(useWorldScriptAI), not just legacy thunks.
- i18n: added settings.ai.grokKey + settings.ai.provider.{gemini,
openai,ollama,anthropic,grok} across all 19 locales -- hand-
translated for the 5 production locales (de/en/es/fr/it), EN
fallback seeded for the other 14 pending the established bulk-
translate workflow. Bundles rebuilt; README's key-count badge/table
updated 2849 -> 2855.
- Tests: providerFactory (grok maps to 'grok', not 'unsupported'),
worldScriptCompletionFetch (grok success path + missing-key 401),
AiProviderCard (renders key input + model selector, saves via
storageService.saveApiKey('grok', ...)).
No CSP change needed: https://api.x.ai is already in
src-tauri/tauri.conf.json, and the web CSP's https: scheme-source
(ADR-0004) already covers it. No new feature flag -- confirmed via
`pnpm exec tsx scripts/audit-feature-parity.ts` (22 flags, 0 drifts).
The many locales/*/{characters,common,dashboard,desktop,export,
outline,worlds,writer}.json changes in this commit are incidental --
running the i18n tooling's --fix pass re-sorted keys in files it
touched along the way. No content was added, removed, or changed in
any of those files, only key order.
@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
worldscript-studioReadyReadyPreviewJul 30, 2026 8:19pm

@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dde94b41-dd6e-4e54-83c5-ef1ed5711dac

📥 Commits

Reviewing files that changed from the base of the PR and between 2689f6d and 744401b.

📒 Files selected for processing (5)
  • components/settings/AiProviderCard.tsx
  • services/ai/worldScriptCompletionFetch.ts
  • tests/unit/listenerMiddleware.test.ts
  • tests/unit/providerFactory.test.ts
  • tests/unit/settings/AiProviderCard.test.tsx
📝 Walkthrough

Walkthrough

Grok is added as a selectable AI provider with API-key persistence, model selection, xAI endpoint configuration, tests, and localized labels. Documentation updates define Grok and Claude integration plans, while locale bundles and project metrics are synchronized.

Changes

AI provider integration

Layer / File(s)Summary
Integration decisions and documentation
GROK-PROVIDER-INTEGRATION-PLAN.md, docs/adr/*, README.md
Integration plans, ADR indexing, testing criteria, Ollama addendum wording, and i18n metrics are updated.
Grok settings and runtime resolution
components/settings/AiProviderCard.tsx, services/ai/*
The settings UI supports Grok keys and models, while provider resolution loads the key and targets https://api.x.ai/v1 with Grok model defaults.
Provider and synchronization tests
tests/unit/ai/*, tests/unit/providerFactory.test.ts, tests/unit/listenerMiddleware.test.ts
Tests cover Grok configuration and missing-key responses, provider-kind mapping, and selector data passed through local-first synchronization.
Localization resources
locales/*/settings.json, public/locales/*/bundle.json
Grok API-key and provider labels are added across settings locales and public bundles alongside synchronized UI translation entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant Settings
participant Storage
participant ProviderFactory
participant ModelConfig
Settings->>Storage: Save Grok API key
Settings->>ProviderFactory: Select grok provider
ProviderFactory->>ModelConfig: Resolve grok kind
ModelConfig->>Storage: Load Grok API key
ModelConfig-->>Settings: Return xAI endpoint and Grok model
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 33.33% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the main change: native Grok provider wiring for ADR-0016 Phase 1.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/grok-native-provider

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GROK-PROVIDER-INTEGRATION-PLAN.md (1)

81-94: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the Phase 1 contract aligned with the landed implementation.

The code now returns a distinct grok kind from providerToKind() and resolves it in worldScriptCompletionFetch.ts; it does not return openaiCompatible directly. Update this instruction and the “confirm whether” wording so future work does not follow a stale contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@GROK-PROVIDER-INTEGRATION-PLAN.md` around lines 81 - 94, Update the Phase 1
instructions for providerFactory.ts to require providerToKind() to return the
distinct grok kind, and state that worldScriptCompletionFetch.ts resolves this
kind and the Grok API key. Remove the “confirm whether” wording and do not
instruct implementers to return openaiCompatible directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/settings/AiProviderCard.tsx`:
- Around line 49-50: Add an immediately adjacent single-line QNBS-v3 rationale
comment for each substantive TypeScript addition: annotate Grok state in
components/settings/AiProviderCard.tsx:49-50, persistence flow at :71-88,
provider-list changes at :192-200, and Grok settings UI at :345-380; annotate
Grok model resolution in services/ai/worldScriptCompletionFetch.ts:84-95; and
annotate missing-key and successful-resolution coverage in
tests/unit/ai/worldScriptCompletionFetch.test.ts:166-174 and :248-278. Keep each
rationale specific to the behavior introduced at its site.
In `@docs/adr/0016-native-grok-and-claude-providers.md`:
- Around line 97-100: Update the “Positive” bullet in the ADR to describe Claude
desktop availability as future, changing the current “becomes usable on desktop
immediately” wording to indicate it will become usable only after Track A ships;
preserve the existing Grok and web Track B statements.
In `@locales/ar/settings.json`:
- Line 137: Localize the newly added Grok provider labels in every affected
locale: translate the Grok API-key label and the “Ollama (local)” label in
locales/ar/settings.json (138, 218-222), locales/el/settings.json (138,
218-222), public/locales/eu/bundle.json (1756, 1836-1840),
public/locales/fa/bundle.json (1756, 1836-1840), public/locales/fi/bundle.json
(1756, 1836-1840), and public/locales/he/bundle.json (1756, 1836-1840), using
Arabic, Greek, Basque, Persian, Finnish, and Hebrew respectively.
In `@locales/he/settings.json`:
- Line 137: Translate the settings.ai.grokKey value in locales/he/settings.json,
locales/hu/settings.json, locales/is/settings.json, and locales/ja/settings.json
into each locale’s language. Translate the newly added heuristic and related
user-facing entries in public/locales/ru/bundle.json, including the specified
ranges, and update the corresponding source locale entries for Portuguese,
Korean, Swedish, and Chinese before regenerating public/locales/pt/bundle.json,
public/locales/ko/bundle.json, public/locales/sv/bundle.json, and
public/locales/zh/bundle.json; do not edit generated bundles independently of
their source files.
In `@locales/pt/settings.json`:
- Line 137: Localize the settings.ai.grokKey label in locales/pt/settings.json
(Chave da API do Grok), locales/ru/settings.json (Ключ API Grok), and
locales/sv/settings.json (Grok API-nyckel), and use the appropriate Chinese
translation in locales/zh/settings.json. Add the corresponding Arabic and Greek
source translations, then regenerate public/locales/ar/bundle.json and
public/locales/el/bundle.json so their settings.ai.grokKey entries are
localized.
In `@services/ai/worldScriptCompletionFetch.ts`:
- Around line 84-95: Add an immediately adjacent one-line QNBS-v3 rationale
comment in the kind === 'grok' configuration block, specifically near the
returned provider, baseURL, apiKey, and modelId mapping. Keep the implementation
unchanged and explain the rationale for this Grok-specific configuration.
In `@tests/unit/listenerMiddleware.test.ts`:
- Around line 74-84: The binding mock currently depends on variables that may be
accessed before initialization because vi.mock is hoisted. Move
MockProjectDocBinding, mockSyncFromProject, mockVerify, mockReproject, and
lastBindingCtorArg into a single vi.hoisted block, then have the
ProjectDocBinding mock factory use those hoisted references while preserving the
existing mock behavior.
In `@tests/unit/providerFactory.test.ts`:
- Around line 67-68: Collapse the QNBS-v3 rationale at
tests/unit/providerFactory.test.ts lines 67-68 into one adjacent single-line
comment. Also collapse the rationales at tests/unit/listenerMiddleware.test.ts
lines 62-63 and 71-73 into one adjacent single-line QNBS-v3 comment each,
preserving their existing explanations.
In `@tests/unit/settings/AiProviderCard.test.tsx`:
- Line 67: Add one-line `// QNBS-v3: ...` rationale comments for the
`grokAdvancedAi` fixture and the Grok test suite covering lines 345-361. Replace
the existing two-line annotation near lines 360-361 with a single-line comment,
keeping each explanation immediately adjacent to the substantive TSX change.
---
Outside diff comments:
In `@GROK-PROVIDER-INTEGRATION-PLAN.md`:
- Around line 81-94: Update the Phase 1 instructions for providerFactory.ts to
require providerToKind() to return the distinct grok kind, and state that
worldScriptCompletionFetch.ts resolves this kind and the Grok API key. Remove
the “confirm whether” wording and do not instruct implementers to return
openaiCompatible directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd57d23c-18b0-4844-be4d-8db459b85bc2

📥 Commits

Reviewing files that changed from the base of the PR and between 6de97a0 and 2689f6d.

📒 Files selected for processing (50)
  • GROK-PROVIDER-INTEGRATION-PLAN.md
  • README.md
  • components/settings/AiProviderCard.tsx
  • docs/adr/0016-native-grok-and-claude-providers.md
  • docs/adr/README.md
  • docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md
  • locales/ar/settings.json
  • locales/de/settings.json
  • locales/el/settings.json
  • locales/en/settings.json
  • locales/es/settings.json
  • locales/eu/settings.json
  • locales/fa/settings.json
  • locales/fi/settings.json
  • locales/fr/settings.json
  • locales/he/settings.json
  • locales/hu/settings.json
  • locales/is/settings.json
  • locales/it/settings.json
  • locales/ja/settings.json
  • locales/ko/settings.json
  • locales/pt/settings.json
  • locales/ru/settings.json
  • locales/sv/settings.json
  • locales/zh/settings.json
  • public/locales/ar/bundle.json
  • public/locales/de/bundle.json
  • public/locales/el/bundle.json
  • public/locales/en/bundle.json
  • public/locales/es/bundle.json
  • public/locales/eu/bundle.json
  • public/locales/fa/bundle.json
  • public/locales/fi/bundle.json
  • public/locales/fr/bundle.json
  • public/locales/he/bundle.json
  • public/locales/hu/bundle.json
  • public/locales/is/bundle.json
  • public/locales/it/bundle.json
  • public/locales/ja/bundle.json
  • public/locales/ko/bundle.json
  • public/locales/pt/bundle.json
  • public/locales/ru/bundle.json
  • public/locales/sv/bundle.json
  • public/locales/zh/bundle.json
  • services/ai/providerFactory.ts
  • services/ai/worldScriptCompletionFetch.ts
  • tests/unit/ai/worldScriptCompletionFetch.test.ts
  • tests/unit/listenerMiddleware.test.ts
  • tests/unit/providerFactory.test.ts
  • tests/unit/settings/AiProviderCard.test.tsx

Comment threadcomponents/settings/AiProviderCard.tsx
Comment threaddocs/adr/0016-native-grok-and-claude-providers.md
Comment threadlocales/ar/settings.json
Comment threadlocales/he/settings.json
Comment threadlocales/pt/settings.json
Comment threadservices/ai/worldScriptCompletionFetch.ts
Comment threadtests/unit/listenerMiddleware.test.ts
Comment threadtests/unit/providerFactory.test.ts Outdated
Comment threadtests/unit/settings/AiProviderCard.test.tsx
@codecov

codecovBot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.36364% with 3 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
components/settings/AiProviderCard.tsx80.00%2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Add missing single-line QNBS-v3 rationale comments (Grok key state,
key-persistence handler, the openaiCompatible-reuse decision in
worldScriptCompletionFetch.ts, two test fixtures/blocks) and collapse
four multi-line QNBS-v3 comments to the required single line.
Not addressed here (documented, not fixed): the ADR-0016 wording
describing Claude desktop as immediately usable is accurate once the
full stacked sequence merges (Track A is PR #300, already stacked next)
and will read correctly at that point without further edits; the
locale-translation findings for settings.ai.grokKey reflect this repo's
established i18n tiering (5 production locales get hand translations,
14 beta locales intentionally carry an EN fallback per CLAUDE.md's own
i18n section), not a defect; the vi.mock hoisting concern in
listenerMiddleware.test.ts is a false positive verified against the
actual passing suite -- ProjectDocBinding is consumed via a dynamic
await import() inside test bodies, not a static top-level import, so
by the time any mock factory runs the module has already evaluated
top-to-bottom once.
@qnbs

qnbs commented Jul 30, 2026

Copy link
Copy Markdown
OwnerAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qnbs
qnbs merged commit 82bd231 into mainJul 30, 2026
22 checks passed
@qnbs
qnbs deleted the feat/grok-native-provider branch July 30, 2026 20:42
@qnbs
qnbs restored the feat/grok-native-provider branch July 30, 2026 20:44
@qnbs
qnbs deleted the feat/grok-native-provider branch July 30, 2026 20:52
qnbs added a commit that referenced this pull request Jul 30, 2026
* fix: address deferred CodeRabbit findings from PR #297
Four findings deferred from PR #297 to keep that PR's merge unblocked,
addressed here first per explicit instruction before starting the
Grok/Claude/Ollama plan execution:
1. GROK-PROVIDER-INTEGRATION-PLAN.md: the addendum was numbered "## 7"
but its own subsections said "8.1"-"8.4", and a "§§1-7 above"
self-reference included the addendum itself. Renumbered to 7.1-7.4,
fixed both self-references, and fixed a second pre-existing
ambiguous "§5" cross-reference (meant "item 5 within Phase 1's own
list", not top-level section 5) while in the area.
2. tests/unit/listenerMiddleware.test.ts: added the required QNBS-v3
comment next to the new featureFlagsActions import.
3. Added the required QNBS-v3 comment before the new "local-first
shadow sync" describe block.
4. Rewrote the local-first sync test to assert the actual
selector-to-binding boundary directly instead of inferring success
from an absence of logger calls in an unmocked-dependency,
environment-dependent setup. Added real mocks for
services/localFirst/{projectDoc,docBinding,docPersistence} and
services/storage/storageEncryptionService so the test can assert
the exact object selectProjectData(state) returns is what reaches
ProjectDocBinding's constructor and syncFromProject/verify.
Hit two real bugs while building this: (a) an arrow-function mock
implementation can never be used as a constructor -- switched to a
real class, which Biome's useArrowFunction rule doesn't flag either
(a function-expression fix would have been immediately reverted by
the same linter that was satisfied by removing the arrow function
in the first place); (b) vi.fn().mockImplementation() can't type a
class constructor against its generic (...args) => any signature --
mocked the class directly instead of wrapping it in vi.fn().
* docs: Phase 0 re-verification finds Claude split -- two tracks, not one
Re-checked the plan's own findings against current main before starting
implementation (Phase 0's explicit purpose), and found the original
draft's Claude analysis was incomplete in a way that changes the whole
approach:
- AiProviderCard.tsx's primary provider dropdown already has an
'anthropic' entry with a dedicated warning block -- missed originally
because the investigation searched for the literal string "claude",
never "anthropic" (the actual identifier), when checking that file.
Its existing i18n copy already half-promises desktop support
("...or use the Tauri desktop app for Anthropic calls").
- streamAnthropic() throws unconditionally on EVERY platform, including
desktop -- but CORS is a browser-only restriction. It never even
checks isTauriRuntime() before throwing. Tauri's native HTTP plugin
(@tauri-apps/plugin-http, services/localServerHttp.ts) already solves
exactly this class of problem for Ollama/LM Studio/vLLM (ADR-0012);
the same escape hatch works for any HTTPS endpoint, including
api.anthropic.com, since native networking isn't subject to browser
CORS at all.
Restructured Phase 2 into two independent tracks: Track A (desktop) is
a narrow bug fix reusing ADR-0012's established pattern, no new
infrastructure, ships first. Track B (web/PWA) is the actual new
architecture -- the serverless proxy -- and is now correctly scoped to
only the three web deploy targets (desktop no longer needs it at all).
Updated the Executive Summary, both Definition of Done checklists, and
all internal cross-references accordingly.
* docs(adr): add ADR-0016 for native Grok provider + split Claude fix
Formalizes GROK-PROVIDER-INTEGRATION-PLAN.md's Phase 0 decision record:
Grok is a pure UI-wiring fix (backend already works); Claude splits
into Track A (desktop, native-HTTP via the ADR-0012 pattern, ships
first, no new infrastructure) and Track B (web/PWA, this app's first
serverless backend dependency, ships second). Also documents why the
Ollama-in-PWA follow-up (Issue #266) is a separate decision, not a
Track-B variant -- a hosted proxy can't reach a user's own localhost.
* feat(ai): Phase 1 -- native Grok provider wiring
Grok (xAI, Cloud 4) had a real, working backend (aiProviderService.ts's
streamGrok(), key storage, a live /v1/models connection test) but was
never selectable as a primary provider -- reachable only as a hybrid-
fallback-chain option. This closes that UI/wiring gap; no new backend
work needed.
- AiProviderCard.tsx: added Grok to the primary provider dropdown with
its own API-key input + model selector (grok-3 / grok-3-mini),
reusing the existing storageService key-storage plumbing and the
already-working testAIConnection() grok case for Test Connection.
Also fixed the whole provider array's i18n while touching it --
gemini/openai/ollama/anthropic labels were hardcoded literals too,
not just Grok's gap.
- providerFactory.ts: providerToKind() gets a distinct 'grok' kind
(not folded into 'openaiCompatible') -- Grok needs a fixed baseURL
(https://api.x.ai/v1) and a real stored key, unlike the Ollama
default that also maps to 'openaiCompatible'; folding it in would
have silently sent Grok requests through Ollama's localhost default.
- worldScriptCompletionFetch.ts: resolveModelConfig() branches on the
new 'grok' kind, bringing Grok into the newer Writer-streaming path
(useWorldScriptAI), not just legacy thunks.
- i18n: added settings.ai.grokKey + settings.ai.provider.{gemini,
openai,ollama,anthropic,grok} across all 19 locales -- hand-
translated for the 5 production locales (de/en/es/fr/it), EN
fallback seeded for the other 14 pending the established bulk-
translate workflow. Bundles rebuilt; README's key-count badge/table
updated 2849 -> 2855.
- Tests: providerFactory (grok maps to 'grok', not 'unsupported'),
worldScriptCompletionFetch (grok success path + missing-key 401),
AiProviderCard (renders key input + model selector, saves via
storageService.saveApiKey('grok', ...)).
No CSP change needed: https://api.x.ai is already in
src-tauri/tauri.conf.json, and the web CSP's https: scheme-source
(ADR-0004) already covers it. No new feature flag -- confirmed via
`pnpm exec tsx scripts/audit-feature-parity.ts` (22 flags, 0 drifts).
The many locales/*/{characters,common,dashboard,desktop,export,
outline,worlds,writer}.json changes in this commit are incidental --
running the i18n tooling's --fix pass re-sorted keys in files it
touched along the way. No content was added, removed, or changed in
any of those files, only key order.
* feat(ai): Phase 2 Track A -- native Claude support on desktop
Claude/Anthropic's streamAnthropic() threw unconditionally on every
platform, including desktop -- but CORS is a browser-only restriction.
Tauri's native HTTP plugin (localServerFetch, ADR-0012) already exists
in this codebase to bypass exactly this class of problem for Ollama;
the same escape hatch works for any HTTPS endpoint, since native
networking isn't subject to browser CORS at all.
- aiProviderService.ts: streamAnthropic() branches on isTauriRuntime()
before throwing -- desktop calls https://api.anthropic.com/v1/messages
directly via localServerFetch (Messages API format: x-api-key +
anthropic-version headers). generateTextSingleProvider()'s anthropic
case now delegates to the fixed streamAnthropic instead of its own
separate unconditional throw. testAIConnection()'s anthropic case
gets the same branch, using a minimal (max_tokens: 1) real request as
the connectivity check since Anthropic has no public /v1/models
endpoint to probe. Image generation is deliberately left throwing --
Anthropic's API doesn't offer that endpoint at all, so this isn't a
CORS bug to fix.
- src-tauri/tauri.conf.json: added https://api.anthropic.com to the
CSP connect-src (mirrors the existing Grok entry).
- AiProviderCard.tsx: the existing 'anthropic' warning-only block is
now desktop-conditional -- desktop renders a real API-key input +
model selector (Opus/Sonnet/Haiku 4.x) exactly like every other
cloud provider; web/PWA keeps the warning, with copy updated to say
desktop now genuinely works rather than "might" (Track B's proxy
note stays "coming soon", since that part isn't built yet).
- i18n: added settings.ai.anthropicKey; revised anthropicCorsNote/
anthropicHint wording across the 5 production locales to reflect
desktop actually working now; EN fallback seeded for the other 14.
2855 -> 2856 keys; README badge/table updated.
- Tests: aiProviderService (testAIConnection + streamText desktop
success/failure/no-key paths, web-path unchanged), AiProviderCard
(desktop shows key input, web shows warning, key save wired to
storageService.saveApiKey('anthropic', ...)).
Track B (the web/PWA serverless proxy) is unaffected by this commit --
still not built, still the only path for browser-tab Claude.
* docs: address CodeRabbit quick-win findings on PR #299
Add missing single-line QNBS-v3 rationale comments (Grok key state,
key-persistence handler, the openaiCompatible-reuse decision in
worldScriptCompletionFetch.ts, two test fixtures/blocks) and collapse
four multi-line QNBS-v3 comments to the required single line.
Not addressed here (documented, not fixed): the ADR-0016 wording
describing Claude desktop as immediately usable is accurate once the
full stacked sequence merges (Track A is PR #300, already stacked next)
and will read correctly at that point without further edits; the
locale-translation findings for settings.ai.grokKey reflect this repo's
established i18n tiering (5 production locales get hand translations,
14 beta locales intentionally carry an EN fallback per CLAUDE.md's own
i18n section), not a defect; the vi.mock hoisting concern in
listenerMiddleware.test.ts is a false positive verified against the
actual passing suite -- ProjectDocBinding is consumed via a dynamic
await import() inside test bodies, not a static top-level import, so
by the time any mock factory runs the module has already evaluated
top-to-bottom once.
* fix(ai): concatenate all Claude text blocks, bound the connectivity ping
Two Major CodeRabbit findings on PR #300, both real:
- streamAnthropic() used content?.find(c => c.type === 'text') -- Claude
can return multiple content blocks (e.g. a 'thinking' block plus
several 'text' blocks on extended-thinking models), and find() silently
dropped everything after the first match. Now filters and concatenates
every text block in order.
- testAIConnection's anthropic ping had no timeout, unlike every sibling
connectivity check (testOllamaConnection: timeoutMs 5000; openai/grok/
gemini: AbortSignal.timeout(8000)) -- a stalled native HTTP call could
hang the Settings test spinner indefinitely. Added timeoutMs: 8000.
Deliberately NOT applied to streamAnthropic's own outbound call (the
second location CodeRabbit's finding named): real generation calls
across this file intentionally stay unbounded by a fixed timeout,
using only the caller's own AbortSignal for cancellation -- Ollama's
real generation call (streamOllama) follows the same pattern, only its
connectivity check gets timeoutMs. Special-casing Anthropic's
generation call would be the inconsistent choice, not the fix.
qnbs added a commit that referenced this pull request Jul 30, 2026
* fix: address deferred CodeRabbit findings from PR #297
Four findings deferred from PR #297 to keep that PR's merge unblocked,
addressed here first per explicit instruction before starting the
Grok/Claude/Ollama plan execution:
1. GROK-PROVIDER-INTEGRATION-PLAN.md: the addendum was numbered "## 7"
but its own subsections said "8.1"-"8.4", and a "§§1-7 above"
self-reference included the addendum itself. Renumbered to 7.1-7.4,
fixed both self-references, and fixed a second pre-existing
ambiguous "§5" cross-reference (meant "item 5 within Phase 1's own
list", not top-level section 5) while in the area.
2. tests/unit/listenerMiddleware.test.ts: added the required QNBS-v3
comment next to the new featureFlagsActions import.
3. Added the required QNBS-v3 comment before the new "local-first
shadow sync" describe block.
4. Rewrote the local-first sync test to assert the actual
selector-to-binding boundary directly instead of inferring success
from an absence of logger calls in an unmocked-dependency,
environment-dependent setup. Added real mocks for
services/localFirst/{projectDoc,docBinding,docPersistence} and
services/storage/storageEncryptionService so the test can assert
the exact object selectProjectData(state) returns is what reaches
ProjectDocBinding's constructor and syncFromProject/verify.
Hit two real bugs while building this: (a) an arrow-function mock
implementation can never be used as a constructor -- switched to a
real class, which Biome's useArrowFunction rule doesn't flag either
(a function-expression fix would have been immediately reverted by
the same linter that was satisfied by removing the arrow function
in the first place); (b) vi.fn().mockImplementation() can't type a
class constructor against its generic (...args) => any signature --
mocked the class directly instead of wrapping it in vi.fn().
* docs: Phase 0 re-verification finds Claude split -- two tracks, not one
Re-checked the plan's own findings against current main before starting
implementation (Phase 0's explicit purpose), and found the original
draft's Claude analysis was incomplete in a way that changes the whole
approach:
- AiProviderCard.tsx's primary provider dropdown already has an
'anthropic' entry with a dedicated warning block -- missed originally
because the investigation searched for the literal string "claude",
never "anthropic" (the actual identifier), when checking that file.
Its existing i18n copy already half-promises desktop support
("...or use the Tauri desktop app for Anthropic calls").
- streamAnthropic() throws unconditionally on EVERY platform, including
desktop -- but CORS is a browser-only restriction. It never even
checks isTauriRuntime() before throwing. Tauri's native HTTP plugin
(@tauri-apps/plugin-http, services/localServerHttp.ts) already solves
exactly this class of problem for Ollama/LM Studio/vLLM (ADR-0012);
the same escape hatch works for any HTTPS endpoint, including
api.anthropic.com, since native networking isn't subject to browser
CORS at all.
Restructured Phase 2 into two independent tracks: Track A (desktop) is
a narrow bug fix reusing ADR-0012's established pattern, no new
infrastructure, ships first. Track B (web/PWA) is the actual new
architecture -- the serverless proxy -- and is now correctly scoped to
only the three web deploy targets (desktop no longer needs it at all).
Updated the Executive Summary, both Definition of Done checklists, and
all internal cross-references accordingly.
* docs(adr): add ADR-0016 for native Grok provider + split Claude fix
Formalizes GROK-PROVIDER-INTEGRATION-PLAN.md's Phase 0 decision record:
Grok is a pure UI-wiring fix (backend already works); Claude splits
into Track A (desktop, native-HTTP via the ADR-0012 pattern, ships
first, no new infrastructure) and Track B (web/PWA, this app's first
serverless backend dependency, ships second). Also documents why the
Ollama-in-PWA follow-up (Issue #266) is a separate decision, not a
Track-B variant -- a hosted proxy can't reach a user's own localhost.
* feat(ai): Phase 1 -- native Grok provider wiring
Grok (xAI, Cloud 4) had a real, working backend (aiProviderService.ts's
streamGrok(), key storage, a live /v1/models connection test) but was
never selectable as a primary provider -- reachable only as a hybrid-
fallback-chain option. This closes that UI/wiring gap; no new backend
work needed.
- AiProviderCard.tsx: added Grok to the primary provider dropdown with
its own API-key input + model selector (grok-3 / grok-3-mini),
reusing the existing storageService key-storage plumbing and the
already-working testAIConnection() grok case for Test Connection.
Also fixed the whole provider array's i18n while touching it --
gemini/openai/ollama/anthropic labels were hardcoded literals too,
not just Grok's gap.
- providerFactory.ts: providerToKind() gets a distinct 'grok' kind
(not folded into 'openaiCompatible') -- Grok needs a fixed baseURL
(https://api.x.ai/v1) and a real stored key, unlike the Ollama
default that also maps to 'openaiCompatible'; folding it in would
have silently sent Grok requests through Ollama's localhost default.
- worldScriptCompletionFetch.ts: resolveModelConfig() branches on the
new 'grok' kind, bringing Grok into the newer Writer-streaming path
(useWorldScriptAI), not just legacy thunks.
- i18n: added settings.ai.grokKey + settings.ai.provider.{gemini,
openai,ollama,anthropic,grok} across all 19 locales -- hand-
translated for the 5 production locales (de/en/es/fr/it), EN
fallback seeded for the other 14 pending the established bulk-
translate workflow. Bundles rebuilt; README's key-count badge/table
updated 2849 -> 2855.
- Tests: providerFactory (grok maps to 'grok', not 'unsupported'),
worldScriptCompletionFetch (grok success path + missing-key 401),
AiProviderCard (renders key input + model selector, saves via
storageService.saveApiKey('grok', ...)).
No CSP change needed: https://api.x.ai is already in
src-tauri/tauri.conf.json, and the web CSP's https: scheme-source
(ADR-0004) already covers it. No new feature flag -- confirmed via
`pnpm exec tsx scripts/audit-feature-parity.ts` (22 flags, 0 drifts).
The many locales/*/{characters,common,dashboard,desktop,export,
outline,worlds,writer}.json changes in this commit are incidental --
running the i18n tooling's --fix pass re-sorted keys in files it
touched along the way. No content was added, removed, or changed in
any of those files, only key order.
* feat(ai): Phase 2 Track A -- native Claude support on desktop
Claude/Anthropic's streamAnthropic() threw unconditionally on every
platform, including desktop -- but CORS is a browser-only restriction.
Tauri's native HTTP plugin (localServerFetch, ADR-0012) already exists
in this codebase to bypass exactly this class of problem for Ollama;
the same escape hatch works for any HTTPS endpoint, since native
networking isn't subject to browser CORS at all.
- aiProviderService.ts: streamAnthropic() branches on isTauriRuntime()
before throwing -- desktop calls https://api.anthropic.com/v1/messages
directly via localServerFetch (Messages API format: x-api-key +
anthropic-version headers). generateTextSingleProvider()'s anthropic
case now delegates to the fixed streamAnthropic instead of its own
separate unconditional throw. testAIConnection()'s anthropic case
gets the same branch, using a minimal (max_tokens: 1) real request as
the connectivity check since Anthropic has no public /v1/models
endpoint to probe. Image generation is deliberately left throwing --
Anthropic's API doesn't offer that endpoint at all, so this isn't a
CORS bug to fix.
- src-tauri/tauri.conf.json: added https://api.anthropic.com to the
CSP connect-src (mirrors the existing Grok entry).
- AiProviderCard.tsx: the existing 'anthropic' warning-only block is
now desktop-conditional -- desktop renders a real API-key input +
model selector (Opus/Sonnet/Haiku 4.x) exactly like every other
cloud provider; web/PWA keeps the warning, with copy updated to say
desktop now genuinely works rather than "might" (Track B's proxy
note stays "coming soon", since that part isn't built yet).
- i18n: added settings.ai.anthropicKey; revised anthropicCorsNote/
anthropicHint wording across the 5 production locales to reflect
desktop actually working now; EN fallback seeded for the other 14.
2855 -> 2856 keys; README badge/table updated.
- Tests: aiProviderService (testAIConnection + streamText desktop
success/failure/no-key paths, web-path unchanged), AiProviderCard
(desktop shows key input, web shows warning, key save wired to
storageService.saveApiKey('anthropic', ...)).
Track B (the web/PWA serverless proxy) is unaffected by this commit --
still not built, still the only path for browser-tab Claude.
* feat(ai): Phase 2 Track B -- Claude serverless proxy for web/PWA
Track A (previous commit) fixed Claude on desktop. This closes the
other half: browser tabs have no native-HTTP escape hatch from CORS,
so Anthropic requests from Vercel/Cloudflare Pages deployments now
relay through this app's own stateless serverless proxy -- its first
backend dependency ever (previously a purely static SPA + a desktop
bundle with no server of its own).
- api/_shared/claudeProxyCore.ts: platform-agnostic relay core (Web
Request/Response only, no @vercel/node or @cloudflare/workers-types
dependency). Forwards { apiKey, model, messages, maxTokens? } to
Anthropic's Messages API and returns the response unmodified. Mandatory
abuse controls per ADR-0016 (the endpoint is public and unauthenticated
by construction -- CWE-400 surface): Zod schema validation, a 256 KiB
body-size cap checked against both the Content-Length header and the
actual body (defeats a spoofed header), a same-origin check (Origin
must match the deployment's own host), a best-effort per-client-IP
rate limit (20 req/60s, in-memory -- explicitly not distributed, see
code comment), and a 20s outbound timeout to Anthropic. Never logs the
key, prompt, or response on any path -- asserted directly in tests via
console spies across every branch.
- api/claude-proxy.ts: Vercel Edge Function entry point (thin wrapper).
- functions/api/claude-proxy.ts: Cloudflare Pages Function equivalent,
at the matching /api/claude-proxy route (Cloudflare has no automatic
"/api" prefix the way some platforms do -- the path has to earn it).
- services/deployTarget.ts: isServerlessProxyCapable() -- reuses
import.meta.env.BASE_URL (already this codebase's build-time
GitHub-Pages-vs-edge marker, see config/resolveViteBase.ts) to detect
whether the current web build can host the proxy at all. GitHub Pages
is static-only and never can.
- services/aiProviderService.ts: streamAnthropic() now has three
branches -- desktop (Track A, unchanged), proxy-capable web (fetches
/api/claude-proxy), and GitHub Pages (throws immediately, before ever
checking for a key, since no key would help). testAIConnection's
anthropic case gets the same three-way split. Retired the
'backendProxyRequired' TestConnectionErrorKind (nothing can produce it
anymore) in favor of 'proxyUnavailableStaticHost'.
- AiProviderCard.tsx: extracted the whole anthropic UI block into
components/settings/AnthropicProviderFields.tsx -- both to keep
AiProviderCard under the Biome cognitive-complexity gate (52 > 50
before the split) and under CLAUDE.md's 700-line file-size target.
Desktop and proxy-capable web now render the same real key + model
UI every other cloud provider gets; only GitHub Pages keeps the
warning block, with copy rewritten from a generic CORS note to the
actual "no proxy host" reason.
- i18n: settings.ai.anthropicProxyNote (new); anthropicCorsNote/
anthropicHint/corsRestriction copy rewritten (desktop and Vercel/CF
no longer say "coming soon" -- they work); testError.
backendProxyRequired removed, testError.proxyUnavailableStaticHost
added. Hand-translated for the 5 production locales; EN fallback
seeded for the other 14. 2856 -> 2857 keys; README badge/table/count
updated in all 4 places.
- docs/SECURITY-THREAT-MODEL.md: new "Claude serverless proxy
trust-model change" section, an Information Disclosure row, a Denial
of Service row (the abuse-control list), a Mitigation Mapping row,
and a checklist item -- this is the first provider whose BYOK key
transits infrastructure WorldScript runs, and that's stated plainly
rather than folded into the general BYOK framing.
- README.md: privacy bullet and the encryption section both carry the
same caveat; the Cloud 3 provider-stack row lists the actual model
ids (Opus 4.7/Sonnet 4.6/Haiku 4.5, not the stale "Claude 3.5
Sonnet") and where Claude does/doesn't work.
- docs/adr/0016: documents why providerFactory.ts's Vercel-AI-SDK layer
still returns 'unsupported' for anthropic (would need the
@ai-sdk/anthropic package -- Anthropic's Messages API isn't
OpenAI-Chat-Completions-shaped like Grok/Ollama are, so that's a new
runtime dependency out of scope here) and why no dedicated E2E spec
was added (no other cloud provider -- including Grok -- has one in
this repo; the RTL/unit level is where this class of Settings-form
coverage already lives).
Tests: 14 new (claudeProxyCore: schema/size/origin/rate-limit/timeout/
relay-fidelity/no-logging), 3 new (Vercel + Cloudflare entry-point
delegation), 3 new (deployTarget branch coverage), aiProviderService's
Anthropic describe block rewritten for the three-way split,
AiProviderCard's anthropic describe block rewritten with a new
proxy-capable-web case. 126 tests green across the full Track B
surface; lint, typecheck, i18n:check, docs:check, and parity:check
(22 flags, 0 drifts -- no new flag needed) all clean.
* docs: address CodeRabbit quick-win findings on PR #299
Add missing single-line QNBS-v3 rationale comments (Grok key state,
key-persistence handler, the openaiCompatible-reuse decision in
worldScriptCompletionFetch.ts, two test fixtures/blocks) and collapse
four multi-line QNBS-v3 comments to the required single line.
Not addressed here (documented, not fixed): the ADR-0016 wording
describing Claude desktop as immediately usable is accurate once the
full stacked sequence merges (Track A is PR #300, already stacked next)
and will read correctly at that point without further edits; the
locale-translation findings for settings.ai.grokKey reflect this repo's
established i18n tiering (5 production locales get hand translations,
14 beta locales intentionally carry an EN fallback per CLAUDE.md's own
i18n section), not a defect; the vi.mock hoisting concern in
listenerMiddleware.test.ts is a false positive verified against the
actual passing suite -- ProjectDocBinding is consumed via a dynamic
await import() inside test bodies, not a static top-level import, so
by the time any mock factory runs the module has already evaluated
top-to-bottom once.
* fix(docs): restore the '+' suffix scripts/sync-readme-metrics.mjs expects
PR #301's Build, E2E Tests, and E2E Deep Coverage jobs all failed at the
same root cause: pnpm run build's predev/prebuild sync step invokes
sync-readme-metrics.mjs, which hard-fails via its own drift guard when a
metric occurrence in README.md doesn't match the real, computed value.
Four README occurrences read "6477 tests / 529 files" (no "+"), while
every regex the script uses -- both its targeted replacements and the
drift guard's generic check -- expects the "6477+ tests" convention the
rest of the file already follows. Because the file count only changed
NOW (529 -> 532, from adding claudeProxyCore.test.ts,
claudeProxyEntrypoints.test.ts, and deployTarget.test.ts across Track B
and the Ollama addendum), the guard fired for the first time -- the "+"-
less phrasing had been silently tolerated as long as the numeric value
happened to still match.
Restored the "+" suffix (this repo's actual, already-established
convention for a fast-changing count, not a new one) rather than adding
a parallel no-"+" regex path to the script -- the script's convention
was correct; my own earlier edit was the drift. Running the script now
auto-updates 529 -> 532 and is idempotent on a second run.
* fix(api): bound the Claude proxy rate-limiter's eviction instead of clearing it (CWE-770)
A spoofed x-forwarded-for burst that pushed the tracked-client map over its
5000-entry cap triggered a wholesale rateLimitLog.clear(), resetting every
real client's rate-limit window — a DoS vector against the limiter itself.
Now evicts stale-window entries first, then oldest-by-insertion-order,
never the current caller's own entry. Also fixes two doc-drift nitpicks
CodeRabbit caught on the same PR: a stale functions/claude-proxy.ts path in
two header comments (actual path is functions/api/claude-proxy.ts), and a
multi-line JSDoc block in AnthropicProviderFields.tsx that should have been
the repo's single-line QNBS-v3 comment format.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Jul 30, 2026
#266) (#302)
* fix: address deferred CodeRabbit findings from PR #297
Four findings deferred from PR #297 to keep that PR's merge unblocked,
addressed here first per explicit instruction before starting the
Grok/Claude/Ollama plan execution:
1. GROK-PROVIDER-INTEGRATION-PLAN.md: the addendum was numbered "## 7"
but its own subsections said "8.1"-"8.4", and a "§§1-7 above"
self-reference included the addendum itself. Renumbered to 7.1-7.4,
fixed both self-references, and fixed a second pre-existing
ambiguous "§5" cross-reference (meant "item 5 within Phase 1's own
list", not top-level section 5) while in the area.
2. tests/unit/listenerMiddleware.test.ts: added the required QNBS-v3
comment next to the new featureFlagsActions import.
3. Added the required QNBS-v3 comment before the new "local-first
shadow sync" describe block.
4. Rewrote the local-first sync test to assert the actual
selector-to-binding boundary directly instead of inferring success
from an absence of logger calls in an unmocked-dependency,
environment-dependent setup. Added real mocks for
services/localFirst/{projectDoc,docBinding,docPersistence} and
services/storage/storageEncryptionService so the test can assert
the exact object selectProjectData(state) returns is what reaches
ProjectDocBinding's constructor and syncFromProject/verify.
Hit two real bugs while building this: (a) an arrow-function mock
implementation can never be used as a constructor -- switched to a
real class, which Biome's useArrowFunction rule doesn't flag either
(a function-expression fix would have been immediately reverted by
the same linter that was satisfied by removing the arrow function
in the first place); (b) vi.fn().mockImplementation() can't type a
class constructor against its generic (...args) => any signature --
mocked the class directly instead of wrapping it in vi.fn().
* docs: Phase 0 re-verification finds Claude split -- two tracks, not one
Re-checked the plan's own findings against current main before starting
implementation (Phase 0's explicit purpose), and found the original
draft's Claude analysis was incomplete in a way that changes the whole
approach:
- AiProviderCard.tsx's primary provider dropdown already has an
'anthropic' entry with a dedicated warning block -- missed originally
because the investigation searched for the literal string "claude",
never "anthropic" (the actual identifier), when checking that file.
Its existing i18n copy already half-promises desktop support
("...or use the Tauri desktop app for Anthropic calls").
- streamAnthropic() throws unconditionally on EVERY platform, including
desktop -- but CORS is a browser-only restriction. It never even
checks isTauriRuntime() before throwing. Tauri's native HTTP plugin
(@tauri-apps/plugin-http, services/localServerHttp.ts) already solves
exactly this class of problem for Ollama/LM Studio/vLLM (ADR-0012);
the same escape hatch works for any HTTPS endpoint, including
api.anthropic.com, since native networking isn't subject to browser
CORS at all.
Restructured Phase 2 into two independent tracks: Track A (desktop) is
a narrow bug fix reusing ADR-0012's established pattern, no new
infrastructure, ships first. Track B (web/PWA) is the actual new
architecture -- the serverless proxy -- and is now correctly scoped to
only the three web deploy targets (desktop no longer needs it at all).
Updated the Executive Summary, both Definition of Done checklists, and
all internal cross-references accordingly.
* docs(adr): add ADR-0016 for native Grok provider + split Claude fix
Formalizes GROK-PROVIDER-INTEGRATION-PLAN.md's Phase 0 decision record:
Grok is a pure UI-wiring fix (backend already works); Claude splits
into Track A (desktop, native-HTTP via the ADR-0012 pattern, ships
first, no new infrastructure) and Track B (web/PWA, this app's first
serverless backend dependency, ships second). Also documents why the
Ollama-in-PWA follow-up (Issue #266) is a separate decision, not a
Track-B variant -- a hosted proxy can't reach a user's own localhost.
* feat(ai): Phase 1 -- native Grok provider wiring
Grok (xAI, Cloud 4) had a real, working backend (aiProviderService.ts's
streamGrok(), key storage, a live /v1/models connection test) but was
never selectable as a primary provider -- reachable only as a hybrid-
fallback-chain option. This closes that UI/wiring gap; no new backend
work needed.
- AiProviderCard.tsx: added Grok to the primary provider dropdown with
its own API-key input + model selector (grok-3 / grok-3-mini),
reusing the existing storageService key-storage plumbing and the
already-working testAIConnection() grok case for Test Connection.
Also fixed the whole provider array's i18n while touching it --
gemini/openai/ollama/anthropic labels were hardcoded literals too,
not just Grok's gap.
- providerFactory.ts: providerToKind() gets a distinct 'grok' kind
(not folded into 'openaiCompatible') -- Grok needs a fixed baseURL
(https://api.x.ai/v1) and a real stored key, unlike the Ollama
default that also maps to 'openaiCompatible'; folding it in would
have silently sent Grok requests through Ollama's localhost default.
- worldScriptCompletionFetch.ts: resolveModelConfig() branches on the
new 'grok' kind, bringing Grok into the newer Writer-streaming path
(useWorldScriptAI), not just legacy thunks.
- i18n: added settings.ai.grokKey + settings.ai.provider.{gemini,
openai,ollama,anthropic,grok} across all 19 locales -- hand-
translated for the 5 production locales (de/en/es/fr/it), EN
fallback seeded for the other 14 pending the established bulk-
translate workflow. Bundles rebuilt; README's key-count badge/table
updated 2849 -> 2855.
- Tests: providerFactory (grok maps to 'grok', not 'unsupported'),
worldScriptCompletionFetch (grok success path + missing-key 401),
AiProviderCard (renders key input + model selector, saves via
storageService.saveApiKey('grok', ...)).
No CSP change needed: https://api.x.ai is already in
src-tauri/tauri.conf.json, and the web CSP's https: scheme-source
(ADR-0004) already covers it. No new feature flag -- confirmed via
`pnpm exec tsx scripts/audit-feature-parity.ts` (22 flags, 0 drifts).
The many locales/*/{characters,common,dashboard,desktop,export,
outline,worlds,writer}.json changes in this commit are incidental --
running the i18n tooling's --fix pass re-sorted keys in files it
touched along the way. No content was added, removed, or changed in
any of those files, only key order.
* feat(ai): Phase 2 Track A -- native Claude support on desktop
Claude/Anthropic's streamAnthropic() threw unconditionally on every
platform, including desktop -- but CORS is a browser-only restriction.
Tauri's native HTTP plugin (localServerFetch, ADR-0012) already exists
in this codebase to bypass exactly this class of problem for Ollama;
the same escape hatch works for any HTTPS endpoint, since native
networking isn't subject to browser CORS at all.
- aiProviderService.ts: streamAnthropic() branches on isTauriRuntime()
before throwing -- desktop calls https://api.anthropic.com/v1/messages
directly via localServerFetch (Messages API format: x-api-key +
anthropic-version headers). generateTextSingleProvider()'s anthropic
case now delegates to the fixed streamAnthropic instead of its own
separate unconditional throw. testAIConnection()'s anthropic case
gets the same branch, using a minimal (max_tokens: 1) real request as
the connectivity check since Anthropic has no public /v1/models
endpoint to probe. Image generation is deliberately left throwing --
Anthropic's API doesn't offer that endpoint at all, so this isn't a
CORS bug to fix.
- src-tauri/tauri.conf.json: added https://api.anthropic.com to the
CSP connect-src (mirrors the existing Grok entry).
- AiProviderCard.tsx: the existing 'anthropic' warning-only block is
now desktop-conditional -- desktop renders a real API-key input +
model selector (Opus/Sonnet/Haiku 4.x) exactly like every other
cloud provider; web/PWA keeps the warning, with copy updated to say
desktop now genuinely works rather than "might" (Track B's proxy
note stays "coming soon", since that part isn't built yet).
- i18n: added settings.ai.anthropicKey; revised anthropicCorsNote/
anthropicHint wording across the 5 production locales to reflect
desktop actually working now; EN fallback seeded for the other 14.
2855 -> 2856 keys; README badge/table updated.
- Tests: aiProviderService (testAIConnection + streamText desktop
success/failure/no-key paths, web-path unchanged), AiProviderCard
(desktop shows key input, web shows warning, key save wired to
storageService.saveApiKey('anthropic', ...)).
Track B (the web/PWA serverless proxy) is unaffected by this commit --
still not built, still the only path for browser-tab Claude.
* feat(ai): Phase 2 Track B -- Claude serverless proxy for web/PWA
Track A (previous commit) fixed Claude on desktop. This closes the
other half: browser tabs have no native-HTTP escape hatch from CORS,
so Anthropic requests from Vercel/Cloudflare Pages deployments now
relay through this app's own stateless serverless proxy -- its first
backend dependency ever (previously a purely static SPA + a desktop
bundle with no server of its own).
- api/_shared/claudeProxyCore.ts: platform-agnostic relay core (Web
Request/Response only, no @vercel/node or @cloudflare/workers-types
dependency). Forwards { apiKey, model, messages, maxTokens? } to
Anthropic's Messages API and returns the response unmodified. Mandatory
abuse controls per ADR-0016 (the endpoint is public and unauthenticated
by construction -- CWE-400 surface): Zod schema validation, a 256 KiB
body-size cap checked against both the Content-Length header and the
actual body (defeats a spoofed header), a same-origin check (Origin
must match the deployment's own host), a best-effort per-client-IP
rate limit (20 req/60s, in-memory -- explicitly not distributed, see
code comment), and a 20s outbound timeout to Anthropic. Never logs the
key, prompt, or response on any path -- asserted directly in tests via
console spies across every branch.
- api/claude-proxy.ts: Vercel Edge Function entry point (thin wrapper).
- functions/api/claude-proxy.ts: Cloudflare Pages Function equivalent,
at the matching /api/claude-proxy route (Cloudflare has no automatic
"/api" prefix the way some platforms do -- the path has to earn it).
- services/deployTarget.ts: isServerlessProxyCapable() -- reuses
import.meta.env.BASE_URL (already this codebase's build-time
GitHub-Pages-vs-edge marker, see config/resolveViteBase.ts) to detect
whether the current web build can host the proxy at all. GitHub Pages
is static-only and never can.
- services/aiProviderService.ts: streamAnthropic() now has three
branches -- desktop (Track A, unchanged), proxy-capable web (fetches
/api/claude-proxy), and GitHub Pages (throws immediately, before ever
checking for a key, since no key would help). testAIConnection's
anthropic case gets the same three-way split. Retired the
'backendProxyRequired' TestConnectionErrorKind (nothing can produce it
anymore) in favor of 'proxyUnavailableStaticHost'.
- AiProviderCard.tsx: extracted the whole anthropic UI block into
components/settings/AnthropicProviderFields.tsx -- both to keep
AiProviderCard under the Biome cognitive-complexity gate (52 > 50
before the split) and under CLAUDE.md's 700-line file-size target.
Desktop and proxy-capable web now render the same real key + model
UI every other cloud provider gets; only GitHub Pages keeps the
warning block, with copy rewritten from a generic CORS note to the
actual "no proxy host" reason.
- i18n: settings.ai.anthropicProxyNote (new); anthropicCorsNote/
anthropicHint/corsRestriction copy rewritten (desktop and Vercel/CF
no longer say "coming soon" -- they work); testError.
backendProxyRequired removed, testError.proxyUnavailableStaticHost
added. Hand-translated for the 5 production locales; EN fallback
seeded for the other 14. 2856 -> 2857 keys; README badge/table/count
updated in all 4 places.
- docs/SECURITY-THREAT-MODEL.md: new "Claude serverless proxy
trust-model change" section, an Information Disclosure row, a Denial
of Service row (the abuse-control list), a Mitigation Mapping row,
and a checklist item -- this is the first provider whose BYOK key
transits infrastructure WorldScript runs, and that's stated plainly
rather than folded into the general BYOK framing.
- README.md: privacy bullet and the encryption section both carry the
same caveat; the Cloud 3 provider-stack row lists the actual model
ids (Opus 4.7/Sonnet 4.6/Haiku 4.5, not the stale "Claude 3.5
Sonnet") and where Claude does/doesn't work.
- docs/adr/0016: documents why providerFactory.ts's Vercel-AI-SDK layer
still returns 'unsupported' for anthropic (would need the
@ai-sdk/anthropic package -- Anthropic's Messages API isn't
OpenAI-Chat-Completions-shaped like Grok/Ollama are, so that's a new
runtime dependency out of scope here) and why no dedicated E2E spec
was added (no other cloud provider -- including Grok -- has one in
this repo; the RTL/unit level is where this class of Settings-form
coverage already lives).
Tests: 14 new (claudeProxyCore: schema/size/origin/rate-limit/timeout/
relay-fidelity/no-logging), 3 new (Vercel + Cloudflare entry-point
delegation), 3 new (deployTarget branch coverage), aiProviderService's
Anthropic describe block rewritten for the three-way split,
AiProviderCard's anthropic describe block rewritten with a new
proxy-capable-web case. 126 tests green across the full Track B
surface; lint, typecheck, i18n:check, docs:check, and parity:check
(22 flags, 0 drifts -- no new flag needed) all clean.
* feat(ai): Addendum -- opt-in browser-Ollama connection for the PWA (ADR-0017, Issue #266)
Issue #266's own comment thread already worked out the correct answer:
CORS is the server's own configuration, not an immutable browser wall.
If a user starts their own Ollama server with OLLAMA_ORIGINS covering
the PWA's exact origin, a direct browser fetch to localhost succeeds --
real, standards-compliant CORS, the same non-magic model NovelCrafter's
own browser-Ollama support uses. This wires that up as an explicit,
default-off opt-in, without touching the "PWA stays desktop-only by
default" guarantee ADR-0012 established.
- features/featureFlags/featureFlagsSlice.ts: new enableBrowserOllama
flag, default off (23rd flag, 7th user opt-in).
- features/featureCatalog.ts: catalog entry (tier: ai, risk: medium,
matches enableVoiceSupport's classification for a similarly-scoped
opt-in) with real gateLocations.
- hooks/useSettingsView.ts: dispatch case for the new flag (caught by
scripts/audit-feature-parity.ts's "toggle fires, Redux doesn't
update" check -- exactly the class of drift that gate exists for).
- services/aiProviderService.ts: testAIConnection's ollama case now
accepts opts.browserOllamaEnabled instead of hard-requiring
isTauriRuntime(); a generic 'unreachable' result remaps to a new
'corsSuspected' kind when running the opt-in browser path -- CORS
rejection and "server genuinely down" are identical TypeErrors at
the JS level, so this is an honestly-hedged heuristic, not a
diagnosis.
- services/localServerHttp.ts: NO functional change -- verified during
implementation that resolveFetch() already returns globalThis.fetch
unconditionally on the web; the "PWA never probes localhost" policy
lived entirely in the UI/testAIConnection call sites, not the
transport layer. Added a comment explaining why, so a future reader
doesn't go looking for a change that was never needed here.
- components/settings/AiProviderCard.tsx (+ AiSections.tsx passing the
flag down): canAttemptOllama = isDesktop || browserOllamaEnabled
replaces every bare isDesktop check gating the ollama auto-probe
effect, Load Models, and Test Connection. When on, an info block
renders the exact `OLLAMA_ORIGINS=<origin> ollama serve` command for
window.location.origin -- always correct for the current deployment,
never a guessed/generic value, since WorldScript ships from multiple
possible origins (unlike NovelCrafter's single fixed SaaS URL).
- docs/adr/0017-pwa-browser-ollama-opt-in.md: new ADR: why this differs
from the Claude proxy pattern (a hosted function can reach the public
internet, never a user's own localhost -- hard networking fact, not
policy), why WorldScript never defaulted this on unlike NovelCrafter,
explicit non-goals (no LAN-IP, no PNA, not the default). ADR-0012
cross-referenced both directions.
- docs/FEATURE-PARITY.md, CLAUDE.md (x2), tests/CLAUDE.md, AGENTS.md
(x2), .github/copilot-instructions.md: flag-count references updated
22->23 total / 6->7 opt-in-off across every doc that enumerates them.
- i18n: settings.featureFlags.enableBrowserOllama, ai.ollamaBrowserOptInTitle/
Body, ai.testError.corsSuspected -- hand-translated for the 5
production locales, EN fallback seeded for the other 14. 2857 -> 2861
keys; README updated in all 4 places.
- Help articles (help.aiStudio.providers.content, help.faq.api.content,
5 production locales): fixed pre-existing stale content discovered
while updating for this work -- wrong model names (Claude 3.5
Sonnet/Grok-2, both outdated), a genuine CSP-vs-CORS factual error in
the Ollama description (CORS blocks localhost, not CSP -- the exact
misconception ADR-0012 already corrected elsewhere), and a missing
OpenRouter entry (shipped as Cloud 5, never documented in either
article). Not scope creep: same articles, same edit, left broken
would misinform users reading about the very feature this PR ships.
- README.md: Ollama provider-stack row now documents both the desktop
native path and the new opt-in browser path with the ADR-0017 link.
Tests: 5 new (testAIConnection's browserOllamaEnabled on/off,
corsSuspected remapping, non-remap of timeout/success), 5 new
(AiProviderCard's flag-off/on UI states, auto-probe, button enablement,
desktop-unaffected), 1 new (useSettingsView's dispatch case), plus the
parametrized featureFlagsSlice case and updated 21->22 toggle count in
FeatureFlagsSection. 336 tests green across the full affected surface;
lint, typecheck, i18n:check, docs:check, and parity:check (23 flags,
0 drifts) all clean.
* fix(tokens): avoid token-audit false-positive from #266 issue references
audit-tokens.mjs's raw-hex rule (/#[0-9a-fA-F]{3,8}\b/) can't distinguish
a CSS color literal from a GitHub issue reference -- "#266" matches
just as well as "#f0f". The previous commit's two brand-new comments
referencing Issue #266 (an info-block comment in AiProviderCard.tsx, a
roadmapTarget string in featureCatalog.ts) pushed the count to 161
against a baseline of 160. Reworded both to drop the "#" immediately
before the digits rather than raising the baseline -- same rule this
repo already applies to biome-ignore suppressions.
* docs: address CodeRabbit quick-win findings on PR #299
Add missing single-line QNBS-v3 rationale comments (Grok key state,
key-persistence handler, the openaiCompatible-reuse decision in
worldScriptCompletionFetch.ts, two test fixtures/blocks) and collapse
four multi-line QNBS-v3 comments to the required single line.
Not addressed here (documented, not fixed): the ADR-0016 wording
describing Claude desktop as immediately usable is accurate once the
full stacked sequence merges (Track A is PR #300, already stacked next)
and will read correctly at that point without further edits; the
locale-translation findings for settings.ai.grokKey reflect this repo's
established i18n tiering (5 production locales get hand translations,
14 beta locales intentionally carry an EN fallback per CLAUDE.md's own
i18n section), not a defect; the vi.mock hoisting concern in
listenerMiddleware.test.ts is a false positive verified against the
actual passing suite -- ProjectDocBinding is consumed via a dynamic
await import() inside test bodies, not a static top-level import, so
by the time any mock factory runs the module has already evaluated
top-to-bottom once.
* fix(docs): restore the '+' suffix scripts/sync-readme-metrics.mjs expects
PR #301's Build, E2E Tests, and E2E Deep Coverage jobs all failed at the
same root cause: pnpm run build's predev/prebuild sync step invokes
sync-readme-metrics.mjs, which hard-fails via its own drift guard when a
metric occurrence in README.md doesn't match the real, computed value.
Four README occurrences read "6477 tests / 529 files" (no "+"), while
every regex the script uses -- both its targeted replacements and the
drift guard's generic check -- expects the "6477+ tests" convention the
rest of the file already follows. Because the file count only changed
NOW (529 -> 532, from adding claudeProxyCore.test.ts,
claudeProxyEntrypoints.test.ts, and deployTarget.test.ts across Track B
and the Ollama addendum), the guard fired for the first time -- the "+"-
less phrasing had been silently tolerated as long as the numeric value
happened to still match.
Restored the "+" suffix (this repo's actual, already-established
convention for a fast-changing count, not a new one) rather than adding
a parallel no-"+" regex path to the script -- the script's convention
was correct; my own earlier edit was the drift. Running the script now
auto-updates 529 -> 532 and is idempotent on a second run.
* fix(api): bound the Claude proxy rate-limiter's eviction instead of clearing it (CWE-770)
A spoofed x-forwarded-for burst that pushed the tracked-client map over its
5000-entry cap triggered a wholesale rateLimitLog.clear(), resetting every
real client's rate-limit window — a DoS vector against the limiter itself.
Now evicts stale-window entries first, then oldest-by-insertion-order,
never the current caller's own entry. Also fixes two doc-drift nitpicks
CodeRabbit caught on the same PR: a stale functions/claude-proxy.ts path in
two header comments (actual path is functions/api/claude-proxy.ts), and a
multi-line JSDoc block in AnthropicProviderFields.tsx that should have been
the repo's single-line QNBS-v3 comment format.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@qnbs