From c0d79efb7c9bd50e560f10cc182d1c95cc8adf9f Mon Sep 17 00:00:00 2001 From: Wenzheng Jiang Date: Fri, 27 Mar 2026 09:12:23 +0900 Subject: [PATCH] feat(miyo): disable miyo on mobile without a remote server URL (#2328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(miyo): disable miyo on mobile without a remote server URL Adds `shouldUseMiyo()` to `miyoUtils.ts` as the single source of truth for whether Miyo should be active. On mobile, Miyo is silently ignored unless an explicit remote server URL is configured, since local service discovery is unavailable on mobile. The three previously duplicated `shouldUseMiyo` checks in RetrieverFactory, findRelevantNotes, and VectorStoreManager now delegate to this shared function. Also drops the redundant `enableSemanticSearchV3` guard — the UI already enforces that enabling Miyo enables semantic search and disabling semantic search disables Miyo. Co-Authored-By: Claude Sonnet 4.6 * fix(miyo): align RelevantNotes UI check with shouldUseMiyo Replaces the local `shouldUseMiyoIndex` predicate (which duplicated the self-host grace-period logic and missed the mobile platform guard) with a direct call to `shouldUseMiyo` from miyoUtils. This ensures the UI's index-state check matches what the backend actually uses, so the "Build Index"/"No relevant notes" state is always correct on mobile. Co-Authored-By: Claude Sonnet 4.6 * test(miyo): update findRelevantNotes mock to use shouldUseMiyo Replace the isSelfHostAccessValid mock with shouldUseMiyo, which is now the single gate checked by findRelevantNotes. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../chat-components/RelevantNotes.tsx | 30 ++----------------- src/miyo/miyoUtils.ts | 25 +++++++++++++++- src/search/RetrieverFactory.ts | 5 ++-- src/search/findRelevantNotes.test.ts | 18 ++++------- src/search/findRelevantNotes.ts | 6 ++-- src/search/vectorStoreManager.ts | 4 +-- 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/components/chat-components/RelevantNotes.tsx b/src/components/chat-components/RelevantNotes.tsx index 70b5466c..3c4eaa88 100644 --- a/src/components/chat-components/RelevantNotes.tsx +++ b/src/components/chat-components/RelevantNotes.tsx @@ -10,7 +10,7 @@ import { useNoteDrag } from "@/hooks/useNoteDrag"; import { cn } from "@/lib/utils"; import { logWarn } from "@/logger"; import { SemanticSearchToggleModal } from "@/components/modals/SemanticSearchToggleModal"; -import type { CopilotSettings } from "@/settings/model"; +import { shouldUseMiyo } from "@/miyo/miyoUtils"; import { findRelevantNotes, getSimilarityCategory, @@ -30,30 +30,6 @@ import { import { Notice, TFile } from "obsidian"; import React, { memo, useCallback, useEffect, useState } from "react"; -const SELF_HOST_GRACE_PERIOD_MS = 15 * 24 * 60 * 60 * 1000; - -/** - * Return true when Miyo-backed semantic index is expected to be active. - * - * @param settings - Current Copilot settings object. - * @returns True when Miyo mode and self-host validation are active. - */ -function shouldUseMiyoIndex(settings: CopilotSettings): boolean { - if (!settings.enableMiyo || !settings.enableSemanticSearchV3) { - return false; - } - - if (settings.selfHostModeValidatedAt == null) { - return false; - } - - if ((settings.selfHostValidationCount ?? 0) >= 3) { - return true; - } - - return Date.now() - settings.selfHostModeValidatedAt < SELF_HOST_GRACE_PERIOD_MS; -} - function useRelevantNotes(refresher: number) { const [relevantNotes, setRelevantNotes] = useState([]); const [signalTick, setSignalTick] = useState(0); @@ -93,9 +69,9 @@ function useHasIndex(notePath: string, refresher: number) { const VectorStoreManager = (await import("@/search/vectorStoreManager")).default; const { getSettings } = await import("@/settings/model"); const settings = getSettings(); - const shouldUseMiyo = shouldUseMiyoIndex(settings); + const useMiyo = shouldUseMiyo(settings); - if (shouldUseMiyo) { + if (useMiyo) { const isEmpty = await VectorStoreManager.getInstance().isIndexEmpty(); setHasIndex(!isEmpty); return; diff --git a/src/miyo/miyoUtils.ts b/src/miyo/miyoUtils.ts index 11c493af..dce29aad 100644 --- a/src/miyo/miyoUtils.ts +++ b/src/miyo/miyoUtils.ts @@ -1,5 +1,6 @@ +import { isSelfHostAccessValid } from "@/plusUtils"; import { CopilotSettings, getSettings } from "@/settings/model"; -import { App, FileSystemAdapter } from "obsidian"; +import { App, FileSystemAdapter, Platform } from "obsidian"; /** * Return the user-configured Miyo server URL, or "" to fall back to local service discovery. @@ -12,6 +13,28 @@ export function getMiyoCustomUrl(settings: CopilotSettings): string { return (settings.miyoServerUrl || "").trim(); } +/** + * Single source of truth for whether Miyo should be used. + * + * Returns false when: + * - `enableMiyo` is off, or + * - self-host access is invalid, or + * - running on mobile without a remote server URL (local service discovery + * is unavailable on mobile, so Miyo can only work via an explicit URL). + * + * Note: `enableSemanticSearchV3` need not be checked — the UI enforces that + * enabling Miyo also enables semantic search, and disabling semantic search + * also disables Miyo. + * + * @param settings - Current Copilot settings. + */ +export function shouldUseMiyo(settings: CopilotSettings): boolean { + if (!settings.enableMiyo || !isSelfHostAccessValid()) { + return false; + } + return !Platform.isMobile || !!getMiyoCustomUrl(settings); +} + /** * Resolve the vault identifier sent to Miyo. * diff --git a/src/search/RetrieverFactory.ts b/src/search/RetrieverFactory.ts index ba57247c..b2c39d33 100644 --- a/src/search/RetrieverFactory.ts +++ b/src/search/RetrieverFactory.ts @@ -1,5 +1,6 @@ import { logInfo, logWarn } from "@/logger"; -import { isSelfHostAccessValid, isSelfHostModeValid } from "@/plusUtils"; +import { isSelfHostModeValid } from "@/plusUtils"; +import { shouldUseMiyo } from "@/miyo/miyoUtils"; import { getSettings, CopilotSettings } from "@/settings/model"; import { App } from "obsidian"; import { SelfHostRetriever, VectorSearchBackend } from "./selfHostRetriever"; @@ -323,7 +324,7 @@ export class RetrieverFactory { * @returns True when Miyo should be used for semantic retrieval. */ private static shouldUseMiyo(settings: CopilotSettings): boolean { - return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid(); + return shouldUseMiyo(settings); } /** diff --git a/src/search/findRelevantNotes.test.ts b/src/search/findRelevantNotes.test.ts index 4346b079..5187b9da 100644 --- a/src/search/findRelevantNotes.test.ts +++ b/src/search/findRelevantNotes.test.ts @@ -2,9 +2,8 @@ import { TFile } from "obsidian"; import { getBacklinkedNotes, getLinkedNotes } from "@/noteUtils"; import { findRelevantNotes } from "@/search/findRelevantNotes"; import { MiyoClient } from "@/miyo/MiyoClient"; -import { getMiyoVault } from "@/miyo/miyoUtils"; +import { getMiyoVault, shouldUseMiyo } from "@/miyo/miyoUtils"; import { getSettings } from "@/settings/model"; -import { isSelfHostAccessValid } from "@/plusUtils"; import VectorStoreManager from "@/search/vectorStoreManager"; jest.mock("@/noteUtils", () => ({ @@ -50,10 +49,7 @@ jest.mock("@/miyo/MiyoClient", () => ({ jest.mock("@/miyo/miyoUtils", () => ({ getMiyoVault: jest.fn(), getMiyoCustomUrl: jest.fn().mockReturnValue(""), -})); - -jest.mock("@/plusUtils", () => ({ - isSelfHostAccessValid: jest.fn(), + shouldUseMiyo: jest.fn(), })); jest.mock("@/logger", () => ({ @@ -75,9 +71,7 @@ function createMarkdownFile(path: string): TFile { describe("findRelevantNotes", () => { const mockedGetSettings = getSettings as jest.MockedFunction; - const mockedIsSelfHostAccessValid = isSelfHostAccessValid as jest.MockedFunction< - typeof isSelfHostAccessValid - >; + const mockedShouldUseMiyo = shouldUseMiyo as jest.MockedFunction; const mockedGetLinkedNotes = getLinkedNotes as jest.MockedFunction; const mockedGetBacklinkedNotes = getBacklinkedNotes as jest.MockedFunction< typeof getBacklinkedNotes @@ -93,7 +87,7 @@ describe("findRelevantNotes", () => { beforeEach(() => { jest.clearAllMocks(); - mockedIsSelfHostAccessValid.mockReturnValue(false); + mockedShouldUseMiyo.mockReturnValue(false); mockedGetSettings.mockReturnValue({ debug: false, miyoServerUrl: "", @@ -180,7 +174,7 @@ describe("findRelevantNotes", () => { }); it("uses Miyo when shouldUseMiyoForRelevantNotes is true (enableMiyo=true and valid self-host)", async () => { - mockedIsSelfHostAccessValid.mockReturnValue(true); + mockedShouldUseMiyo.mockReturnValue(true); mockedGetSettings.mockReturnValue({ debug: false, miyoServerUrl: "http://127.0.0.1:8742", @@ -256,7 +250,7 @@ describe("findRelevantNotes", () => { }); it("falls back to link-only relevance when Miyo related-note search fails", async () => { - mockedIsSelfHostAccessValid.mockReturnValue(true); + mockedShouldUseMiyo.mockReturnValue(true); mockedGetSettings.mockReturnValue({ debug: false, miyoServerUrl: "http://127.0.0.1:8742", diff --git a/src/search/findRelevantNotes.ts b/src/search/findRelevantNotes.ts index 77fec2da..02f4d8e4 100644 --- a/src/search/findRelevantNotes.ts +++ b/src/search/findRelevantNotes.ts @@ -1,12 +1,11 @@ import { logInfo, logWarn } from "@/logger"; import { MiyoClient } from "@/miyo/MiyoClient"; -import { getMiyoCustomUrl, getMiyoVault } from "@/miyo/miyoUtils"; +import { getMiyoCustomUrl, getMiyoVault, shouldUseMiyo } from "@/miyo/miyoUtils"; import { getBacklinkedNotes, getLinkedNotes } from "@/noteUtils"; import { DBOperations } from "@/search/dbOperations"; import type { SemanticIndexDocument } from "@/search/indexBackend/SemanticIndexBackend"; import VectorStoreManager from "@/search/vectorStoreManager"; import { getSettings } from "@/settings/model"; -import { isSelfHostAccessValid } from "@/plusUtils"; import { InternalTypedDocument, Orama, Result } from "@orama/orama"; import { TFile } from "obsidian"; @@ -20,8 +19,7 @@ const LINKS_WEIGHT = 0.3; * @returns True when Miyo mode and self-host access validation are active. */ function shouldUseMiyoForRelevantNotes(): boolean { - const settings = getSettings(); - return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid(); + return shouldUseMiyo(getSettings()); } /** diff --git a/src/search/vectorStoreManager.ts b/src/search/vectorStoreManager.ts index 97a344aa..5b0d7772 100644 --- a/src/search/vectorStoreManager.ts +++ b/src/search/vectorStoreManager.ts @@ -4,7 +4,7 @@ import { updateIndexingProgressState } from "@/aiParams"; import { CustomError } from "@/error"; import { logInfo, logWarn } from "@/logger"; import EmbeddingsManager from "@/LLMProviders/embeddingManager"; -import { isSelfHostAccessValid } from "@/plusUtils"; +import { shouldUseMiyo } from "@/miyo/miyoUtils"; import { CopilotSettings, getSettings, subscribeToSettingsChange } from "@/settings/model"; import { Orama } from "@orama/orama"; import { Notice, Platform, TFile } from "obsidian"; @@ -211,7 +211,7 @@ export default class VectorStoreManager { * @returns True when Miyo should be the active backend. */ private shouldUseMiyo(settings: CopilotSettings): boolean { - return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid(); + return shouldUseMiyo(settings); } /**