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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`'unreachable'`. 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 (#266).** The generic connection-status
badge in `AiProviderCard` and the "desktop app required" banner were driven by two independent,
never-reconciled state signals — `testStatus` (defaults to `idle` → "Ready"/"Bereit") and
`isDesktop`. Since the auto-test effect and the manual "Test connection" button are both disabled
for Ollama in a browser, `testStatus` could never leave its idle default there, so the badge always
read "Ready" right next to a banner saying the opposite. It now shows a distinct "Not available in
browser" label instead whenever `provider === 'ollama' && !isDesktop`, localized in all 19 locales;
desktop is unaffected. Also: `scanLocalOpenAiCompatibleEndpoints()` now tries Ollama's native
`/api/tags` first, falling back to the OpenAI-compat `/v1/models` shim only on failure (mirrors
`testOllamaConnection`/`listOllamaModels`'s existing native-first approach), so an older Ollama
install without the compat shim isn't missed by the scan.

- **Feature-catalog / slice default drift made structurally impossible.** `features/featureCatalog.ts`
now covers all **22** flags (was 16) and **derives** each entry's `defaultOn` from the slice's
`defaultFeatureFlagsState` instead of hand-keying it — the class of bug where the catalog said
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b

**PR review-comment policy — the CodeAnt Correction Loop (proactive, automatic, every PR):** Fix ALL inline comments (CodeAnt AI + any bot/human) on every PR, **without being asked**. Validate findings against the *current* code (anchors may be stale); implement real **root-cause** fixes (code **+ tests + i18n + docs** in lockstep) or reply with evidence if a false positive. **Never add a new `biome-ignore`** — the suppression ratchet (`scripts/check-suppressions.mjs`) fails the quality gate; refactor so the rule passes honestly. Reply to each thread citing the resolving commit (`POST .../comments/<id>/replies`), resolve it (GraphQL `resolveReviewThread`), leave **0 unresolved**. Then commit, push, and **re-trigger** (`gh pr comment <N> --body "@codeant-ai review"`). **Iron rule — loop until quiescent:** a push triggers a fresh review that often raises NEW findings (a "wave"); repeat the full cycle until **BOTH** a fresh review yields **0 new comments** AND **0 threads unresolved**. Only then merge (auto-squash; admin-squash only after CI is green + loop quiescent). Full canonical procedure: [`docs/CODEANT-REVIEW-LOOP.md`](docs/CODEANT-REVIEW-LOOP.md).

**PR-size limit — keep every PR under ~100 changed files so CodeAnt actually reviews it.** CodeAnt does **not** post inline review comments on PRs that exceed ~100 changed files (the >100-file check hangs/skips). Since any i18n-touching change fans out across 17 locale source files + 17 rebuilt `bundle.json` per module, a multi-feature branch crosses 100 fast. **Before pushing, run `git diff --name-only <base>...HEAD | wc -l`.** If it is over ~100, split the work into the **fewest** stacked PRs that each stay clearly under the limit — group by which locale module-files they touch so the per-PR fan-out stays small (e.g. P0 batch touching `writer.json`; P1/P2 batch touching `common.json`/`dashboard.json`). Stack them (PR2 base = PR1's branch) so each PR's incremental diff — what CodeAnt sees — is small; when PR1 merges, PR2 auto-retargets to `main`. **Do not** make more PRs than needed: if everything fits under ~100 in one (or two) PRs, use that. Keep commits atomic per concern regardless of how they are bundled into PRs.
**PR-size limit — keep every PR under ~100 changed files so CodeAnt actually reviews it.** CodeAnt does **not** post inline review comments on PRs that exceed ~100 changed files (the >100-file check hangs/skips). Since any i18n-touching change fans out across 19 locale source files + 19 rebuilt `bundle.json` per module, a multi-feature branch crosses 100 fast. **Before pushing, run `git diff --name-only <base>...HEAD | wc -l`.** If it is over ~100, split the work into the **fewest** stacked PRs that each stay clearly under the limit — group by which locale module-files they touch so the per-PR fan-out stays small (e.g. P0 batch touching `writer.json`; P1/P2 batch touching `common.json`/`dashboard.json`). Stack them (PR2 base = PR1's branch) so each PR's incremental diff — what CodeAnt sees — is small; when PR1 merges, PR2 auto-retargets to `main`. **Do not** make more PRs than needed: if everything fits under ~100 in one (or two) PRs, use that. Keep commits atomic per concern regardless of how they are bundled into PRs.

**E2E notes:** Do NOT use `networkidle` waits (HMR keeps WebSocket open). Scope sidebar navigation via `#sidebar`. Shared helpers: `tests/e2e/helpers.ts`. Mobile E2E: set `RUN_MOBILE_E2E=1` locally (off by default).

Expand Down Expand Up @@ -102,8 +102,8 @@ services/ → External adapters; key sub-dirs:
packages/ → Internal workspace packages: ai-core (WebLLM + inference worker), ui,
collab-transport (vendor fork of y-webrtc 10.3.0 with RTCDataChannel E2E encryption),
worker-bus (typed worker pool, circuit breakers, dead-letter queue — see § WorkerBus below)
locales/ → i18n source JSON (de/en/es/fr/it/ar/he/el/ja/pt/zh/fi/sv/hu/is/eu/fa × 20 modules); runtime: public/locales/<lang>/bundle.json
ar/ + he/ + fa/ — RTL (fa is Arabic-script Persian); el/ja/pt/zh/fi/sv/hu/is/eu — Beta locales (P1-5 + Phase X)
locales/ → i18n source JSON — 19 locales (de/en/es/fr/it/ar/he/el/ja/pt/zh/fi/sv/hu/is/eu/fa/ru/ko × 21 modules); runtime: public/locales/<lang>/bundle.json
ar/ + he/ + fa/ — RTL (fa is Arabic-script Persian); el/ja/pt/zh — Near-production; fi/sv/hu/is/eu/ru/ko — Beta (Phase X + Tier-1 2026 expansion). SSOT: i18n/locales.ts
tests/ → unit/ (Vitest) + e2e/ (Playwright); shared E2E helpers in tests/e2e/helpers.ts
types/ → Supplemental TypeScript definitions (duckdb-wasm-worker.d.ts, tauri-plugins.d.ts)
types.ts → Core shared interfaces and types (root level)
Expand Down Expand Up @@ -252,7 +252,7 @@ Experimental features are gated behind `features/featureFlags/featureFlagsSlice.

### i18n

Custom React Context in `I18nContext.tsx` — not i18next. Source locales: **de, en, es, fr, it** (core), **ar, he, fa** (RTL; fa = Persian/Arabic script), **el, ja, pt, zh, fi, sv, hu, is, eu** (Beta). All 17 ship as `public/locales/<lang>/bundle.json` rebuilt by `pnpm run i18n:bundle` or auto via `pnpm run i18n:check`. All user-facing strings must use `t('key.path')` from `useTranslation()`. New keys: add to **all 17** locale trees (`node scripts/check-i18n-keys.mjs --fix`), then `pnpm run i18n:bundle`. The `/i18n-key` skill targets the **5 core** locales only; update Beta/RTL locales manually afterward. See [`docs/LANGUAGE-EXPANSION-2026.md`](docs/LANGUAGE-EXPANSION-2026.md) for the fi/sv/hu/is/eu/fa rollout and the user-run bulk-translate workflow.
Custom React Context in `I18nContext.tsx` — not i18next. SSOT for locale metadata: `i18n/locales.ts` (`LOCALES` — code/name/status/script/LanguageTool tier; everything else derives from it). **19** source locales: **de, en, es, fr, it** (production), **ja, zh, pt, el** (near-production — full key parity, `help.json` still falls back to English), **ar, he, fa** (RTL beta; fa = Persian/Arabic script), **fi, sv, hu, is, eu, ru, ko** (beta). All 19 ship as `public/locales/<lang>/bundle.json` rebuilt by `pnpm run i18n:bundle` or auto via `pnpm run i18n:check`. All user-facing strings must use `t('key.path')` from `useTranslation()`. New keys: add to **all 19** locale trees (`node scripts/check-i18n-keys.mjs --fix`), then `pnpm run i18n:bundle`. The `/i18n-key` skill targets the **5 production** locales only; update the remaining 14 manually afterward. See [`docs/LANGUAGE-EXPANSION-2026.md`](docs/LANGUAGE-EXPANSION-2026.md) for the fi/sv/hu/is/eu/fa rollout and the user-run bulk-translate workflow (ru/ko followed the same Tier-1 2026 expansion pattern).

**RTL stubs (B-5):** `locales/ar/` + `locales/he/` are English-fallback stubs behind `enableRtlLayout`. Full content is v2.0 community task.

Expand Down
31 changes: 22 additions & 9 deletions components/settings/AiProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
// QNBS-v3 (T0): canonical detection (`__TAURI_INTERNALS__`-aware); `__TAURI__` alone read as web
// in the real desktop shell, hiding desktop-only provider affordances.
const isDesktop = isTauriRuntime();
// QNBS-v3: for Ollama in a browser, the auto-test effect and the manual "Test connection"
// button are both disabled (see below) — testStatus can never leave 'idle' here, so the
// generic status badge must not render its idle→"Ready" label, which would misleadingly
// imply a verified connection next to the "desktop app required" banner.
const ollamaUntestable = provider === 'ollama' && !isDesktop;
const [openaiKey, setOpenaiKey] = useState('');
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'ok' | 'error'>('idle');
Expand Down Expand Up @@ -205,19 +210,27 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
</span>
<span
className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold ${
testStatus === 'ok'
? 'bg-[var(--sc-success-bg)] text-[var(--sc-success-fg)]'
: testStatus === 'error'
? 'bg-[var(--sc-danger-bg)] text-[var(--sc-danger-fg)]'
: 'bg-[var(--sc-surface-overlay)] text-[var(--sc-text-secondary)]'
ollamaUntestable
? 'bg-[var(--sc-surface-overlay)] text-[var(--sc-text-secondary)]'
: testStatus === 'ok'
? 'bg-[var(--sc-success-bg)] text-[var(--sc-success-fg)]'
: testStatus === 'error'
? 'bg-[var(--sc-danger-bg)] text-[var(--sc-danger-fg)]'
: 'bg-[var(--sc-surface-overlay)] text-[var(--sc-text-secondary)]'
}`}
>
{testStatus === 'ok' && t('settings.ai.providerStatusConnected')}
{testStatus === 'error' && t('settings.ai.providerStatusDisconnected')}
{testStatus === 'idle' && t('settings.ai.providerStatusReady')}
{ollamaUntestable ? (
t('settings.ai.providerStatusUnavailableBrowser')
) : (
<>
{testStatus === 'ok' && t('settings.ai.providerStatusConnected')}
{testStatus === 'error' && t('settings.ai.providerStatusDisconnected')}
{testStatus === 'idle' && t('settings.ai.providerStatusReady')}
</>
)}
</span>
</div>
{testStatus === 'error' && testError && (
{!ollamaUntestable && testStatus === 'error' && testError && (
<p className="text-xs text-[var(--sc-danger-fg)]">{testError}</p>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions locales/ar/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "غير متاح",
"settings.ai.providerStatusLabel": "حالة الاتصال",
"settings.ai.providerStatusReady": "جاهز",
"settings.ai.providerStatusUnavailableBrowser": "غير متوفر في المتصفح",
"settings.ai.providerTitle": "مزوّد الذكاء الاصطناعي",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM ‏(المتصفّح)",
Expand Down
1 change: 1 addition & 0 deletions locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Nicht erreichbar",
"settings.ai.providerStatusLabel": "Verbindungsstatus",
"settings.ai.providerStatusReady": "Bereit",
"settings.ai.providerStatusUnavailableBrowser": "Im Browser nicht verfügbar",
"settings.ai.providerTitle": "KI-Anbieter",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (Browser)",
Expand Down
1 change: 1 addition & 0 deletions locales/el/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Δυσεύρετος",
"settings.ai.providerStatusLabel": "Κατάσταση σύνδεσης",
"settings.ai.providerStatusReady": "Ετοιμος",
"settings.ai.providerStatusUnavailableBrowser": "Μη διαθέσιμο στο πρόγραμμα περιήγησης",
"settings.ai.providerTitle": "Πάροχος AI",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (πρόγραμμα περιήγησης)",
Expand Down
1 change: 1 addition & 0 deletions locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Unavailable",
"settings.ai.providerStatusLabel": "Connection Status",
"settings.ai.providerStatusReady": "Ready",
"settings.ai.providerStatusUnavailableBrowser": "Not available in browser",
"settings.ai.providerTitle": "AI Provider",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (browser)",
Expand Down
1 change: 1 addition & 0 deletions locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "No disponible",
"settings.ai.providerStatusLabel": "Estado de conexión",
"settings.ai.providerStatusReady": "Listo",
"settings.ai.providerStatusUnavailableBrowser": "No disponible en el navegador",
"settings.ai.providerTitle": "Proveedor de IA",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (navegador)",
Expand Down
1 change: 1 addition & 0 deletions locales/eu/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Ez dago erabilgarri",
"settings.ai.providerStatusLabel": "Konexioaren egoera",
"settings.ai.providerStatusReady": "Prest",
"settings.ai.providerStatusUnavailableBrowser": "Ez dago erabilgarri nabigatzailean",
"settings.ai.providerTitle": "AI hornitzailea",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (arakatzailea)",
Expand Down
1 change: 1 addition & 0 deletions locales/fa/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "در دسترس نیست",
"settings.ai.providerStatusLabel": "وضعیت اتصال",
"settings.ai.providerStatusReady": "آماده است",
"settings.ai.providerStatusUnavailableBrowser": "در مرورگر در دسترس نیست",
"settings.ai.providerTitle": "ارائه دهنده هوش مصنوعی",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (مرورگر)",
Expand Down
1 change: 1 addition & 0 deletions locales/fi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Ei saatavilla",
"settings.ai.providerStatusLabel": "Yhteyden tila",
"settings.ai.providerStatusReady": "Valmis",
"settings.ai.providerStatusUnavailableBrowser": "Ei saatavilla selaimessa",
"settings.ai.providerTitle": "AI Provider",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (selain)",
Expand Down
1 change: 1 addition & 0 deletions locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Indisponible",
"settings.ai.providerStatusLabel": "État de la connexion",
"settings.ai.providerStatusReady": "Prêt",
"settings.ai.providerStatusUnavailableBrowser": "Non disponible dans le navigateur",
"settings.ai.providerTitle": "Fournisseur d'IA",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (navigateur)",
Expand Down
1 change: 1 addition & 0 deletions locales/he/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "לא זמין",
"settings.ai.providerStatusLabel": "סטטוס חיבור",
"settings.ai.providerStatusReady": "מוכן",
"settings.ai.providerStatusUnavailableBrowser": "לא זמין בדפדפן",
"settings.ai.providerTitle": "ספק AI",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM ‏(דפדפן)",
Expand Down
1 change: 1 addition & 0 deletions locales/hu/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Nem elérhető",
"settings.ai.providerStatusLabel": "Kapcsolat állapota",
"settings.ai.providerStatusReady": "Kész",
"settings.ai.providerStatusUnavailableBrowser": "Böngészőben nem érhető el",
"settings.ai.providerTitle": "AI szolgáltató",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (böngésző)",
Expand Down
1 change: 1 addition & 0 deletions locales/is/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Ekki tiltækt",
"settings.ai.providerStatusLabel": "Tengingarstaða",
"settings.ai.providerStatusReady": "Tilbúið",
"settings.ai.providerStatusUnavailableBrowser": "Ekki í boði í vafra",
"settings.ai.providerTitle": "AI veitandi",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (vafri)",
Expand Down
1 change: 1 addition & 0 deletions locales/it/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Non disponibile",
"settings.ai.providerStatusLabel": "Stato della connessione",
"settings.ai.providerStatusReady": "Pronto",
"settings.ai.providerStatusUnavailableBrowser": "Non disponibile nel browser",
"settings.ai.providerTitle": "Provider IA",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (browser)",
Expand Down
1 change: 1 addition & 0 deletions locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "利用不可",
"settings.ai.providerStatusLabel": "接続ステータス",
"settings.ai.providerStatusReady": "準備ができて",
"settings.ai.providerStatusUnavailableBrowser": "ブラウザでは利用できません",
"settings.ai.providerTitle": "AIプロバイダー",
"settings.ai.providerTransformers": "トランスフォーマー.js",
"settings.ai.providerWebllm": "WebLLM (ブラウザ)",
Expand Down
1 change: 1 addition & 0 deletions locales/ko/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "없는",
"settings.ai.providerStatusLabel": "연결 상태",
"settings.ai.providerStatusReady": "준비가 된",
"settings.ai.providerStatusUnavailableBrowser": "브라우저에서 사용할 수 없음",
"settings.ai.providerTitle": "AI 제공업체",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM(브라우저)",
Expand Down
1 change: 1 addition & 0 deletions locales/pt/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Indisponível",
"settings.ai.providerStatusLabel": "Status da conexão",
"settings.ai.providerStatusReady": "Preparar",
"settings.ai.providerStatusUnavailableBrowser": "Não disponível no navegador",
"settings.ai.providerTitle": "IA Provider",
"settings.ai.providerTransformers": "Transformadores.js",
"settings.ai.providerWebllm": "WebLLM (navegador)",
Expand Down
1 change: 1 addition & 0 deletions locales/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Недоступно",
"settings.ai.providerStatusLabel": "Статус соединения",
"settings.ai.providerStatusReady": "Готовый",
"settings.ai.providerStatusUnavailableBrowser": "Недоступно в браузере",
"settings.ai.providerTitle": "Поставщик ИИ",
"settings.ai.providerTransformers": "Трансформеры.js",
"settings.ai.providerWebllm": "WebLLM (браузер)",
Expand Down
1 change: 1 addition & 0 deletions locales/sv/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "Inte tillgänglig",
"settings.ai.providerStatusLabel": "Anslutningsstatus",
"settings.ai.providerStatusReady": "Redo",
"settings.ai.providerStatusUnavailableBrowser": "Inte tillgängligt i webbläsaren",
"settings.ai.providerTitle": "AI-leverantör",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM (webbläsare)",
Expand Down
1 change: 1 addition & 0 deletions locales/zh/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"settings.ai.providerStatusDisconnected": "不可用",
"settings.ai.providerStatusLabel": "连接状态",
"settings.ai.providerStatusReady": "准备好",
"settings.ai.providerStatusUnavailableBrowser": "浏览器中不可用",
"settings.ai.providerTitle": "人工智能提供商",
"settings.ai.providerTransformers": "Transformers.js",
"settings.ai.providerWebllm": "WebLLM(浏览器)",
Expand Down
Loading
Loading