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
5 changes: 4 additions & 1 deletion components/settings/AiProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
testAIConnection,
} from '../../services/aiProviderService';
import { storageService } from '../../services/storageService';
import { isTauriRuntime } from '../../services/tauriRuntime';
import type { AdvancedAiSettings, AIProvider, LocalBackendPreset } from '../../types';
import { Button } from '../ui/Button';
import { Card, CardContent, CardHeader } from '../ui/Card';
Expand All @@ -35,7 +36,9 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
const { t } = useTranslation();
const provider = advancedAi.provider;
const ollamaBaseUrl = advancedAi.ollamaBaseUrl;
const isDesktop = typeof window !== 'undefined' && Boolean(window.__TAURI__);
// 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();
const [openaiKey, setOpenaiKey] = useState('');
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'ok' | 'error'>('idle');
Expand Down
6 changes: 5 additions & 1 deletion services/ai/fetchAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { isTauriRuntime } from '../tauriRuntime';

type TauriHttpFetch = typeof globalThis.fetch;
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;

let cachedTauriFetch: TauriHttpFetch | null | undefined;

async function resolveTauriFetch(): Promise<TauriHttpFetch | undefined> {
if (cachedTauriFetch !== undefined) return cachedTauriFetch ?? undefined;
if (typeof window === 'undefined' || !window.__TAURI__) {
// QNBS-v3 (T0): canonical detection — `window.__TAURI__` alone was false in the real shell, so
// desktop AI calls never used the native HTTP client (CORS bypass) and silently hit the WebView.
if (!isTauriRuntime()) {
cachedTauriFetch = null;
return undefined;
}
Expand Down
5 changes: 4 additions & 1 deletion services/ai/localAiDeviceProfiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Detects WebGPU, WebNN, NPU, Compute Shaders, memory tier, battery, and platform.
*/

import { isTauriRuntime } from '../tauriRuntime';
import { detectWebGpuDetails } from './webGpuDetectorService';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -202,7 +203,9 @@ function detectMemoryTier(): DeviceCapabilityProfile['memoryTier'] {
// ---------------------------------------------------------------------------

function detectPlatform(): DevicePlatform {
const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
// QNBS-v3 (T0): canonical detection (`__TAURI_INTERNALS__`-aware); `__TAURI__` alone misclassified
// the real desktop shell as web, picking the wrong device profile.
const isTauri = isTauriRuntime();
const isMobile =
typeof navigator !== 'undefined' && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);

Expand Down
5 changes: 4 additions & 1 deletion services/aiProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
testOllamaConnection,
} from './ollamaService';
import { storageService } from './storageService';
import { isTauriRuntime } from './tauriRuntime';

const providerTextSchema = z.object({
text: z.string().min(1),
Expand Down Expand Up @@ -684,7 +685,9 @@ export async function testAIConnection(
return { ok: true };
}
case 'ollama': {
const isDesktop = typeof window !== 'undefined' && Boolean(window.__TAURI__);
// QNBS-v3 (T0): canonical detection — `__TAURI__` alone was false in the real shell, so the
// desktop Ollama (localhost) path was unreachable there.
const isDesktop = isTauriRuntime();
if (!isDesktop) {
return {
ok: false,
Expand Down
6 changes: 5 additions & 1 deletion services/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// QNBS-v3: B-6 StructuredLogger — replaces ring-buffer with IDB + Tauri JSONL sinks

// QNBS-v3 (T0): canonical Tauri detection (now `__TAURI_INTERNALS__`-aware). tauriRuntime is a
// dependency-free leaf, so importing it into the logger introduces no cycle.
import { isTauriRuntime } from './tauriRuntime';

const isDev = typeof import.meta !== 'undefined' && Boolean(import.meta.env?.DEV);

// --- Types ------------------------------------------------------------------
Expand Down Expand Up @@ -115,7 +119,7 @@ async function loadTauriSink(): Promise<{
: null;
}
_tauriChecked = true;
if (typeof window === 'undefined' || !('__TAURI__' in window)) return null;
if (!isTauriRuntime()) return null;
try {
const [fsM, pathM] = await Promise.all([
import('@tauri-apps/plugin-fs'),
Expand Down
8 changes: 3 additions & 5 deletions services/lora/loraTrainingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import { v4 as uuid } from 'uuid';
import type { HyperparamPreset } from '../../features/lora/types';
import { logger } from '../logger';
// QNBS-v3 (T0): use the canonical hardened detector instead of a local `__TAURI_INTERNALS__` check
// (the drift this consolidates). isTauriRuntime() now accepts `__TAURI_INTERNALS__` too.
import { isTauriRuntime as isTauri } from '../tauriRuntime';

export interface TrainingJobConfig {
projectId: string;
Expand Down Expand Up @@ -39,11 +42,6 @@ export interface TrainingResult {
ggufPath: string;
}

/** Whether we're running inside the Tauri desktop shell. */
function isTauri(): boolean {
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
}

/** Invoke a Tauri command, typed. Throws on web build. */
async function tauriInvoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
if (!isTauri()) throw new Error('Tauri not available');
Expand Down
6 changes: 4 additions & 2 deletions services/storageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {
import { dbService } from './dbService';
import { fileSystemService } from './fileSystemService';
import { logger } from './logger';
import { isTauriRuntime } from './tauriRuntime';

declare global {
interface Window {
Expand All @@ -43,8 +44,9 @@ class StorageManager {
}

private async initializeBackend(): Promise<void> {
// Check if we're running in Tauri
if (typeof window !== 'undefined' && window.__TAURI__) {
// QNBS-v3 (T0): use the canonical isTauriRuntime() (now `__TAURI_INTERNALS__`-aware) instead of
// a raw `window.__TAURI__` check, which was false in the real shell and forced IndexedDB.
if (isTauriRuntime()) {
try {
await fileSystemService.initialize();
this.backend = fileSystemService;
Expand Down
11 changes: 8 additions & 3 deletions services/tauriRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/** Desktop (Tauri) runtime helpers — dynamic imports keep web bundle lean. */

export function isTauriRuntime(): boolean {
return (
typeof window !== 'undefined' && Boolean((window as Window & { __TAURI__?: unknown }).__TAURI__)
);
if (typeof window === 'undefined') return false;
// QNBS-v3 (T0): Tauri v2 injects `__TAURI_INTERNALS__` unconditionally; `__TAURI__` is added only
// when `app.withGlobalTauri` is enabled (default false — and this app does not set it). Checking
// `__TAURI__` alone made isTauriRuntime() return false inside the real desktop shell, dead-ending
// the entire JS desktop layer (menu/updater/deep-link bridges, `is-desktop` styling). Accept
// either global — matches the robust detection already used in register-sw.ts.
const w = window as Window & { __TAURI_INTERNALS__?: unknown; __TAURI__?: unknown };
return Boolean(w.__TAURI_INTERNALS__) || Boolean(w.__TAURI__);
}

export async function getTauriAppVersion(): Promise<string | null> {
Expand Down
12 changes: 10 additions & 2 deletions tests/unit/tauriRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,25 @@ describe('isTauriRuntime', () => {
expect(isTauriRuntime()).toBe(false);
});

it('returns false when __TAURI__ is not set on window', async () => {
it('returns false when neither Tauri global is set on window', async () => {
vi.stubGlobal('window', {});
const { isTauriRuntime } = await import('../../services/tauriRuntime');
expect(isTauriRuntime()).toBe(false);
});

it('returns true when __TAURI__ is set on window', async () => {
it('returns true when __TAURI__ is set on window (withGlobalTauri)', async () => {
vi.stubGlobal('window', { __TAURI__: {} });
const { isTauriRuntime } = await import('../../services/tauriRuntime');
expect(isTauriRuntime()).toBe(true);
});

// QNBS-v3 (T0): the real desktop shell exposes `__TAURI_INTERNALS__` (always) but not `__TAURI__`
// unless withGlobalTauri is on — which this app does not set. Detection must accept it.
it('returns true when only __TAURI_INTERNALS__ is set (real Tauri v2 shell)', async () => {
vi.stubGlobal('window', { __TAURI_INTERNALS__: {} });
const { isTauriRuntime } = await import('../../services/tauriRuntime');
expect(isTauriRuntime()).toBe(true);
});
});

describe('getTauriAppVersion', () => {
Expand Down
Loading