diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index 3f1ba24..d5aab5c 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -341,7 +341,6 @@ const ChatBox: React.FC = ({ isVisible }) => { const localRuntimeMessage = localModelState.runtimeSupport.reason; const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported; - const hasLocalRuntimeWarning = localModelState.runtimeSupport.supported && !!localRuntimeMessage; return (
@@ -395,11 +394,6 @@ const ChatBox: React.FC = ({ isVisible }) => {

{LOCAL_LLM_DISPLAY_NAME} Local Runtime

- {hasLocalRuntimeWarning && ( -
- {localRuntimeMessage} -
- )} {hasLocalRuntimeHardFailure && (
{localRuntimeMessage} diff --git a/src/components/settings/sections/GeneralSettings.test.tsx b/src/components/settings/sections/GeneralSettings.test.tsx index 8bdf3b0..2ece260 100644 --- a/src/components/settings/sections/GeneralSettings.test.tsx +++ b/src/components/settings/sections/GeneralSettings.test.tsx @@ -119,7 +119,7 @@ describe('GeneralSettings', () => { }); }); - it('shows a warning when runtime may fail on this host but is still allowed', async () => { + it('keeps local runtime available when runtime may fail on this host', async () => { localModelState.runtimeSupport = { supported: true, webgpuExposed: true, @@ -131,7 +131,8 @@ describe('GeneralSettings', () => { render(); - expect(await screen.findByText(/may not support the local browser runtime reliably/i)).toBeTruthy(); + expect(await screen.findByText('Gemma 4 E4B Local Runtime')).toBeTruthy(); + expect(screen.queryByText(/may not support the local browser runtime reliably/i)).toBeNull(); expect(screen.getByText(/The local model downloads automatically/i)).toBeTruthy(); }); }); diff --git a/src/components/settings/sections/GeneralSettings.tsx b/src/components/settings/sections/GeneralSettings.tsx index dc5bad9..dcdde21 100644 --- a/src/components/settings/sections/GeneralSettings.tsx +++ b/src/components/settings/sections/GeneralSettings.tsx @@ -234,7 +234,6 @@ const GeneralSettings: React.FC = () => { const localRuntimeMessage = localModelState.runtimeSupport.reason; const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported; - const hasLocalRuntimeWarning = localModelState.runtimeSupport.supported && !!localRuntimeMessage; // NOTE: Gemini and Claude are not supported yet due to CORS issues. return ( @@ -286,12 +285,6 @@ const GeneralSettings: React.FC = () => {

{LOCAL_LLM_DISPLAY_NAME} Local Runtime

- {hasLocalRuntimeWarning && ( -
- {localRuntimeMessage} -
- )} - {hasLocalRuntimeHardFailure && (
{localRuntimeMessage} diff --git a/src/util/localLLMModelManager.ts b/src/util/localLLMModelManager.ts index d55295f..99354ff 100644 --- a/src/util/localLLMModelManager.ts +++ b/src/util/localLLMModelManager.ts @@ -49,9 +49,11 @@ export class LocalLLMModelManager { } public static async refresh(): Promise { + const runtimeSupport = detectLocalLLMRuntimeSupport(); + this.logSoftRuntimeWarning(runtimeSupport); this.setState({ isChecking: true, - runtimeSupport: detectLocalLLMRuntimeSupport(), + runtimeSupport, }); try { await this.cleanupLegacyEntries(); @@ -66,6 +68,7 @@ export class LocalLLMModelManager { public static async ensureRuntimeSupported(): Promise { const runtimeSupport = detectLocalLLMRuntimeSupport(); + this.logSoftRuntimeWarning(runtimeSupport); this.setState({ runtimeSupport }); if (!runtimeSupport.supported) { throw new Error(runtimeSupport.reason ?? 'Local browser LLM is not supported in this browser.'); @@ -102,6 +105,17 @@ export class LocalLLMModelManager { } } + private static logSoftRuntimeWarning(runtimeSupport: LocalLLMRuntimeSupport): void { + if (runtimeSupport.supported && runtimeSupport.reason) { + console.warn('[localLLM] Runtime warning:', runtimeSupport.reason, { + secureContext: runtimeSupport.secureContext, + webgpuExposed: runtimeSupport.webgpuExposed, + crossOriginIsolated: runtimeSupport.crossOriginIsolated, + sharedArrayBufferAvailable: runtimeSupport.sharedArrayBufferAvailable, + }); + } + } + private static async cleanupLegacyEntries(): Promise { await Promise.all( LOCAL_LLM_LEGACY_FILENAMES.map(async legacyFilename => { diff --git a/src/util/opfsModelCache.ts b/src/util/opfsModelCache.ts index 1b6202e..02044f6 100644 --- a/src/util/opfsModelCache.ts +++ b/src/util/opfsModelCache.ts @@ -7,18 +7,15 @@ export interface ModelDownloadProgress { interface OpfsModelCacheOptions { directoryName?: string; sizeSuffix?: string; - tempSuffix?: string; } export class OpfsModelCache { private readonly directoryName: string; private readonly sizeSuffix: string; - private readonly tempSuffix: string; constructor(options: OpfsModelCacheOptions = {}) { this.directoryName = options.directoryName ?? 'models'; this.sizeSuffix = options.sizeSuffix ?? '.size'; - this.tempSuffix = options.tempSuffix ?? '.download'; } public async exists(filename: string): Promise { @@ -62,8 +59,6 @@ export class OpfsModelCache { const dir = await this.getDir(); await this.removeIfExists(dir, filename); await this.removeIfExists(dir, this.getSizeFilename(filename)); - await this.removeIfExists(dir, `${filename}${this.tempSuffix}`); - await this.removeIfExists(dir, `${this.getSizeFilename(filename)}${this.tempSuffix}`); } public async download( @@ -92,9 +87,8 @@ export class OpfsModelCache { const dir = await this.getDir(); await this.delete(filename); - const tempFilename = `${filename}${this.tempSuffix}`; - const tempHandle = await dir.getFileHandle(tempFilename, { create: true }); - const tempWritable = await tempHandle.createWritable(); + const finalHandle = await dir.getFileHandle(filename, { create: true }); + const finalWritable = await finalHandle.createWritable(); const reader = stream.getReader(); let receivedBytes = 0; @@ -103,7 +97,7 @@ export class OpfsModelCache { const { done, value } = await reader.read(); if (done) break; if (!value) continue; - await tempWritable.write(value); + await finalWritable.write(value); receivedBytes += value.byteLength; onProgress?.({ receivedBytes, @@ -111,32 +105,13 @@ export class OpfsModelCache { percent: totalBytes ? (receivedBytes / totalBytes) * 100 : 0, }); } - await tempWritable.close(); + await finalWritable.close(); const sizeValue = totalBytes ?? receivedBytes; if (!Number.isFinite(sizeValue) || sizeValue <= 0) { throw new Error('Model download did not provide a valid size.'); } - console.log(`[opfsModelCache] Finalizing cached model ${filename} from temp file ${tempFilename}.`); - const finalHandle = await dir.getFileHandle(filename, { create: true }); - const finalWritable = await finalHandle.createWritable(); - try { - const tempFile = await tempHandle.getFile(); - const tempBuffer = await tempFile.arrayBuffer(); - console.log('[opfsModelCache] Temp file ready for finalize copy.', { - filename, - tempFilename, - tempSize: tempFile.size, - expectedSize: sizeValue, - }); - await finalWritable.write(tempBuffer); - await finalWritable.close(); - } catch (error) { - await finalWritable.abort(); - throw error; - } - const sizeHandle = await dir.getFileHandle(this.getSizeFilename(filename), { create: true }); const sizeWritable = await sizeHandle.createWritable(); try { @@ -158,14 +133,13 @@ export class OpfsModelCache { }); } catch (error) { try { - await tempWritable.abort(); + await finalWritable.abort(); } catch { // Ignore abort cleanup errors. } await this.delete(filename); throw error; } finally { - await this.removeIfExists(dir, tempFilename); reader.releaseLock(); } }