From f8c5719dbe2d4f3e96238566f3c14bc7a59f2a6a Mon Sep 17 00:00:00 2001 From: Dishit Date: Sat, 20 Jun 2026 13:24:49 +0530 Subject: [PATCH 1/2] fix: disable persistent file logging to stop UI stalls/ANRs Versions 0.0.88+ routed every logger.* call through a synchronous string build plus a react-native-fs append on the JS thread - capture() was not gated by __DEV__, so it ran in production. With ~480 call sites across hot paths (tool loop, LiteRT, downloads, whisper), this contended with the bridge and contributed to the 20s+ button-lag / ANR reports. logger.* is now a no-op in release builds (dev still mirrors to the console). Removes the appendPersistentLog / formatArg / write-queue / 2MB-trim machinery entirely. No behavior change beyond logging. Co-Authored-By: Dishit Karia --- src/utils/logger.ts | 59 --------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index f17f31aeb..998763c8b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,72 +1,13 @@ -import RNFS from 'react-native-fs'; - -const LOG_FILE_NAME = 'download-debug.log'; -const MAX_LOG_FILE_BYTES = 2 * 1024 * 1024; -const RETAINED_LOG_LINES = 4000; - -let writeQueue = Promise.resolve(); - -function getLogFilePath(): string { - return `${RNFS.DocumentDirectoryPath}/${LOG_FILE_NAME}`; -} - -function formatArg(arg: unknown): string { - if (arg instanceof Error) { - return `${arg.name}: ${arg.message}${arg.stack ? `\n${arg.stack}` : ''}`; - } - if (typeof arg === 'string') return arg; - if (typeof arg === 'number' || typeof arg === 'boolean' || arg == null) return String(arg); - try { - return JSON.stringify(arg); - } catch { - return String(arg); - } -} - -function appendPersistentLog(level: 'log' | 'warn' | 'error', args: unknown[]): void { - const timestamp = new Date().toISOString(); - const line = `[${timestamp}] ${level.toUpperCase()}: ${args.map(formatArg).join(' ')}\n`; - - writeQueue = writeQueue.then(async () => { - try { - const path = getLogFilePath(); - if (await RNFS.exists(path)) { - await RNFS.appendFile(path, line, 'utf8'); - } else { - await RNFS.writeFile(path, line, 'utf8'); - } - - const stat = await RNFS.stat(path); - const size = typeof stat.size === 'string' ? Number.parseInt(stat.size, 10) : stat.size; - if (size > MAX_LOG_FILE_BYTES) { - const content = await RNFS.readFile(path, 'utf8'); - const trimmed = content.split('\n').filter(Boolean).slice(-RETAINED_LOG_LINES).join('\n'); - await RNFS.writeFile(path, trimmed ? `${trimmed}\n` : '', 'utf8'); - } - } catch { - // Logging must never break app execution. - } - }); -} - -function capture(level: 'log' | 'warn' | 'error', args: unknown[]): void { - appendPersistentLog(level, args); -} - const logger = { log: (...args: unknown[]): void => { - capture('log', args); if (__DEV__) console.log(...args); // NOSONAR }, warn: (...args: unknown[]): void => { - capture('warn', args); if (__DEV__) console.warn(...args); // NOSONAR }, error: (...args: unknown[]): void => { - capture('error', args); if (__DEV__) console.error(...args); // NOSONAR }, - getLogFilePath, }; export default logger; From cd729ce6c3c664c41d9295047c28525fb1d1e405 Mon Sep 17 00:00:00 2001 From: Dishit Date: Sat, 20 Jun 2026 13:41:16 +0530 Subject: [PATCH 2/2] fix: detect Ollama vision capability from capabilities array Newer Ollama versions (v0.6.4+) report multimodal support via a top-level `capabilities` array (e.g. ["vision", "tools"]) rather than via model_info keys. The old code only checked model_info, so models like Gemma 4 were always detected as non-vision. Now checks capabilities array first, falls back to model_info key scan, then projector_info keys. Also wires supportsToolCalling from the capabilities array. Co-Authored-By: Dishit Karia --- src/stores/remoteModelCapabilities.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/stores/remoteModelCapabilities.ts b/src/stores/remoteModelCapabilities.ts index cbf09ca00..73ab8d715 100644 --- a/src/stores/remoteModelCapabilities.ts +++ b/src/stores/remoteModelCapabilities.ts @@ -43,10 +43,25 @@ function extractOllamaCapabilities(data: Record): RemoteModelIn let contextLength = 4096; let supportsVision = false; + // Newer Ollama versions expose a top-level `capabilities` array (e.g. ["vision", "tools"]). + // Gemma 4 and similar models use this field instead of model_info keys. + let supportsToolCalling: boolean | undefined; + if (Array.isArray(data.capabilities)) { + const caps = data.capabilities as unknown[]; + supportsVision = caps.includes('vision'); + supportsToolCalling = caps.includes('tools'); + } + if (data.model_info && typeof data.model_info === 'object') { const parsed = parseModelInfoKeys(data.model_info as Record); if (parsed.contextLength > 0) contextLength = parsed.contextLength; - supportsVision = parsed.supportsVision; + if (!supportsVision) supportsVision = parsed.supportsVision; + } + + // projector_info is present for multimodal models when capabilities array is missing. + if (!supportsVision && data.projector_info && typeof data.projector_info === 'object') { + const projectorKeys = Object.keys(data.projector_info as Record); + supportsVision = projectorKeys.some(k => k.includes('vision') || k.includes('clip')); } if (contextLength === 4096 && typeof data.parameters === 'string') { @@ -63,7 +78,7 @@ function extractOllamaCapabilities(data: Record): RemoteModelIn /\.Think|\.Thinking|\.IsThinkSet/.test(template) || /^RENDERER\s/m.test(modelfile); - return { contextLength, supportsVision, supportsThinking }; + return { contextLength, supportsVision, supportsToolCalling, supportsThinking }; } /**